+
+
+
+
Please select due date.
+
-
+
Custom Fields:
@@ -1263,7 +1301,6 @@ async function registerMFAKey() {
let response = await fetch('/ui/webauthn/register_start');
let options = await response.json();
- console.log(JSON.stringify(options));
// Convert challenge from Base64URL to Base64, then to Uint8Array
const challengeBase64 = base64UrlToBase64(options.publicKey.challenge);
options.publicKey.challenge = Uint8Array.from(atob(challengeBase64), c => c.charCodeAt(0));
diff --git a/src/controller/models.rs b/src/controller/models.rs
index c6dee30..378de2f 100644
--- a/src/controller/models.rs
+++ b/src/controller/models.rs
@@ -302,6 +302,9 @@ pub struct CreateAccountRequest {
pub renew_interval_days: Option
,
// expiration
pub expires_at: Option,
+ // due
+ pub due_at: Option,
+ // 2023-12-02T05:11:50.543995
// The custom fields of the account.
pub custom_name: Option>,
// The custom fields of the account.
@@ -344,6 +347,7 @@ impl CreateAccountRequest {
notes: None,
renew_interval_days: None,
expires_at: None,
+ due_at: None,
custom_name: None,
custom_value: None,
password_min_uppercase: None,
@@ -380,6 +384,7 @@ impl CreateAccountRequest {
account.details.icon = self.icon.clone();
account.details.renew_interval_days = self.renew_interval_days;
account.details.expires_at = self.expires_at;
+ account.details.due_at = self.due_at;
account.credentials.password = self.password.clone();
account.credentials.form_fields = self.form_fields.clone().unwrap_or_default();
@@ -524,11 +529,16 @@ pub struct AccountResponse {
// renew interval
pub renew_interval_days: Option,
// expiration
- pub expires_at: Option,
+ pub expires_at: Option,
+ pub expired: bool,
+ // due
+ pub due_at: Option,
+ pub overdue: bool,
+ // 2023-12-02T05:11:50.543995
// The metadata for dates of the account.
- pub credentials_updated_at: Option,
+ pub credentials_updated_at: Option,
// The metadata for date when password was analyzed.
- pub analyzed_at: Option,
+ pub analyzed_at: Option,
// minimum number of upper_case letters should be included.
pub password_min_uppercase: Option,
// minimum number of lower_case letters should be included.
@@ -583,12 +593,27 @@ impl AccountResponse {
password_min_length: Some(account.credentials.password_policy.min_length),
password_max_length: Some(account.credentials.password_policy.max_length),
renew_interval_days: account.details.renew_interval_days,
- expires_at: account.details.expires_at,
- credentials_updated_at: account.details.credentials_updated_at,
- analyzed_at: account.details.analyzed_at,
+ expires_at: None,
+ expired: account.details.is_expired(),
+ due_at: None,
+ overdue: account.details.is_due(),
+ credentials_updated_at: None,
+ analyzed_at: None,
created_at: account.created_at,
updated_at: account.updated_at,
};
+ if let Some(expires_at) = &account.details.expires_at {
+ res.expires_at = Some(expires_at.format("%Y-%m-%d").to_string())
+ }
+ if let Some(due_at) = &account.details.due_at {
+ res.due_at = Some(due_at.format("%Y-%m-%d").to_string())
+ }
+ if let Some(credentials_updated_at) = &account.details.credentials_updated_at {
+ res.credentials_updated_at = Some(credentials_updated_at.format("%Y-%m-%d").to_string())
+ }
+ if let Some(analyzed_at) = &account.details.analyzed_at {
+ res.analyzed_at = Some(analyzed_at.format("%Y-%m-%d").to_string())
+ }
if let Some(ref otp) = res.otp {
res.generated_otp = Option::from(TOTP::new(otp).generate(30, Utc::now().timestamp() as u64));
}
@@ -685,7 +710,9 @@ pub struct UpdateAccountRequest {
// renew interval
pub renew_interval_days: Option,
// expiration
- pub expires_at: Option,
+ pub expires_at: Option, // 2023-12-02T05:11:50.543995
+ // due
+ pub due_at: Option, // 2023-12-02T05:11:50.543995
// minimum number of upper_case letters should be included.
pub password_min_uppercase: Option,
@@ -728,6 +755,7 @@ impl UpdateAccountRequest {
custom_value: None,
renew_interval_days: None,
expires_at: None,
+ due_at: None,
password_min_uppercase: None,
password_min_lowercase: None,
password_min_digits: None,
@@ -769,6 +797,7 @@ impl UpdateAccountRequest {
account.credentials.otp = self.otp.clone();
account.details.renew_interval_days = self.renew_interval_days;
account.details.expires_at = self.expires_at;
+ account.details.due_at = self.due_at;
let mut password_policy = PasswordPolicy::new();
@@ -869,6 +898,7 @@ impl Account {
"icon" => account.details.icon = Some(value),
"renew_interval_days" => account.details.renew_interval_days = Some(value.parse::().unwrap_or(0)),
"expires_at" => account.details.expires_at = safe_parse_string_date(Option::from(value)),
+ "due_at" => account.details.due_at= safe_parse_string_date(Option::from(value)),
"custom_name" => custom_names.push(value),
"custom_value" => custom_values.push(value),
_ => {}
@@ -1058,3 +1088,48 @@ pub struct QueryAccountParams {
pub q: Option,
}
+#[cfg(test)]
+mod tests {
+ use chrono::Utc;
+ use crate::controller::models::{AccountResponse, UpdateAccountRequest};
+ use crate::domain::models::{Account, AccountKind};
+
+ #[test]
+ fn test_should_serialize_account() {
+ let mut account = Account::new("vault0", AccountKind::Logins);
+ account.details.label = Some("my label".into());
+ account.details.version = 10;
+ account.details.favorite = true;
+ account.details.username = Some("test1".into());
+ account.credentials.password = Some("pass".into());
+ account.credentials.notes = Some("my notes".into());
+ account.details.email = Some("email@mail.cc".into());
+ account.details.website_url = Some("https://mail.cc".into());
+ account.details.category = Some("Contacts".into());
+ account.details.tags = vec!["Personal".to_string()];
+ account.details.renew_interval_days = Some(3);
+ account.details.expires_at = Some(Utc::now().naive_utc());
+ account.details.due_at = Some(Utc::now().naive_utc());
+ let account_response = AccountResponse::new(&account);
+ let account_json = serde_json::to_string(&account_response).unwrap();
+ let des_update_account: UpdateAccountRequest = serde_json::from_str(&account_json).unwrap();
+ let des_account = des_update_account.to_account();
+ assert_eq!(account.vault_id, des_account.vault_id);
+ assert_eq!(account.details.account_id, des_account.details.account_id);
+ assert_eq!(account.details.version, des_account.details.version);
+ assert_eq!(account.details.label, des_account.details.label);
+ assert_eq!(account.details.kind, des_account.details.kind);
+ assert_eq!(account.details.favorite, des_account.details.favorite);
+ assert_eq!(account.details.risk, des_account.details.risk);
+ assert_eq!(account.details.username, des_account.details.username);
+ assert_eq!(account.details.email, des_account.details.email);
+ assert_eq!(account.details.category, des_account.details.category);
+ assert_eq!(account.details.website_url, des_account.details.website_url);
+ assert_eq!(account.credentials.password, des_account.credentials.password);
+ assert_eq!(account.credentials.notes, des_account.credentials.notes);
+ assert_eq!(account.details.tags, des_account.details.tags);
+ assert_eq!(account.details.renew_interval_days, des_account.details.renew_interval_days);
+ assert_eq!(account.details.expires_at, des_account.details.expires_at);
+ assert_eq!(account.details.due_at , des_account.details.due_at);
+ }
+}
diff --git a/src/csv.rs b/src/csv.rs
index b50c719..aa23cea 100644
--- a/src/csv.rs
+++ b/src/csv.rs
@@ -11,7 +11,8 @@ pub(crate) struct CSVRecord {
#[serde(rename = "Type", alias = "kind", alias = "type")]
type_: String,
#[serde(alias = "label", alias = "title")]
- name: String, // label
+ name: String,
+ // label
#[serde(alias = "website", alias = "url")]
website_url: Option,
username: Option,
@@ -29,6 +30,7 @@ pub(crate) struct CSVRecord {
icon: Option,
renew_interval_days: Option,
expires_at: Option,
+ due_at: Option,
favorite: Option,
}
@@ -36,7 +38,7 @@ impl CSVRecord {
pub(crate) fn new(account: &Account) -> Self {
Self {
type_: account.details.kind.to_string(),
- name : account.details.label.clone().unwrap_or("".into()),
+ name: account.details.label.clone().unwrap_or("".into()),
website_url: account.details.website_url.clone(),
username: account.details.username.clone(),
email: account.details.email.clone(),
@@ -50,7 +52,8 @@ impl CSVRecord {
tags: Some(account.details.tags.clone().join(";")),
icon: account.details.icon.clone(),
renew_interval_days: account.details.renew_interval_days,
- expires_at: account.details.expires_at.map(|expires_at| format!("{}", expires_at.format("2015-09-05"))),
+ expires_at: account.details.expires_at.map(|expires_at| format!("{}", expires_at.format("%Y-%m-%d %H:%M:%S"))),
+ due_at: account.details.due_at.map(|due_at| format!("{}", due_at.format("%Y-%m-%d %H:%M:%S"))),
favorite: Some(account.details.favorite),
}
}
@@ -89,13 +92,16 @@ impl CSVRecord {
account.details.website_url = self.website_url.clone();
account.details.category = self.category.clone();
if let Some(tags) = &self.tags {
- account.details.tags = tags.as_str().split("[,;]").map(|s|s.to_string()).collect::>();
+ account.details.tags = tags.as_str().split("[,;]").map(|s| s.to_string()).collect::>();
}
account.details.icon = self.icon.clone();
account.details.renew_interval_days = self.renew_interval_days;
if let Some(expires_at) = &self.expires_at {
account.details.expires_at = safe_parse_str_date(expires_at);
}
+ if let Some(due_at) = &self.due_at {
+ account.details.due_at = safe_parse_str_date(due_at);
+ }
account.details.favorite = self.favorite.unwrap_or(false);
account.credentials.password = self.password.clone();
@@ -110,7 +116,9 @@ impl CSVRecord {
#[cfg(test)]
mod tests {
+ use chrono::Utc;
use crate::csv::CSVRecord;
+ use crate::domain::models::{Account, AccountKind};
#[test]
fn test_should_parse_csv() {
@@ -149,4 +157,45 @@ Login,License ,https://www.dol.com/,mylogin6,mypassword6,mynote6,,
let loaded = CSVRecord::parse(&buf).unwrap();
assert_eq!(14, loaded.len());
}
+
+ #[test]
+ fn test_should_serialize_account() {
+ let mut account = Account::new("vault0", AccountKind::Logins);
+ account.details.label = Some("my label".into());
+ account.details.version = 10;
+ account.details.favorite = true;
+ account.details.username = Some("test1".into());
+ account.credentials.password = Some("pass".into());
+ account.credentials.notes = Some("my notes".into());
+ account.details.email = Some("email@mail.cc".into());
+ account.details.website_url = Some("https://mail.cc".into());
+ account.details.category = Some("Contacts".into());
+ account.details.tags = vec!["Personal".to_string()];
+ account.details.renew_interval_days = Some(3);
+ account.details.expires_at = Some(Utc::now().naive_utc());
+ account.details.due_at = Some(Utc::now().naive_utc());
+ let csv_rec = CSVRecord::new(&account);
+ let account_json = serde_json::to_string(&csv_rec).unwrap();
+ let des_csv_rec: CSVRecord = serde_json::from_str(&account_json).unwrap();
+ let des_account = des_csv_rec.to_account(&account.vault_id);
+ assert_eq!(account.vault_id, des_account.vault_id);
+ assert_ne!(account.details.account_id, des_account.details.account_id); // should not be equal
+ assert_ne!(account.details.version, des_account.details.version); // should not be equal
+ assert_eq!(account.details.label, des_account.details.label);
+ assert_eq!(account.details.kind, des_account.details.kind);
+ assert_eq!(account.details.favorite, des_account.details.favorite);
+ assert_eq!(account.details.risk, des_account.details.risk);
+ assert_eq!(account.details.username, des_account.details.username);
+ assert_eq!(account.details.email, des_account.details.email);
+ assert_eq!(account.details.category, des_account.details.category);
+ assert_eq!(account.details.website_url, des_account.details.website_url);
+ assert_eq!(account.credentials.password, des_account.credentials.password);
+ assert_eq!(account.credentials.notes, des_account.credentials.notes);
+ assert_eq!(account.details.tags, des_account.details.tags);
+ assert_eq!(account.details.renew_interval_days, des_account.details.renew_interval_days);
+ assert_eq!(account.details.expires_at.unwrap().format("%Y-%m-%d").to_string(),
+ des_account.details.expires_at.unwrap().format("%Y-%m-%d").to_string());
+ assert_eq!(account.details.due_at.unwrap().format("%Y-%m-%d").to_string(),
+ des_account.details.due_at.unwrap().format("%Y-%m-%d").to_string());
+ }
}
diff --git a/src/domain/args.rs b/src/domain/args.rs
index 4188673..2fb7ed2 100644
--- a/src/domain/args.rs
+++ b/src/domain/args.rs
@@ -190,6 +190,9 @@ pub enum CommandActions {
/// expiration
#[arg(long)]
expires_at: Option,
+ /// due-at
+ #[arg(long)]
+ due_at: Option,
},
UpdateAccount {
/// id of account
@@ -266,6 +269,10 @@ pub enum CommandActions {
/// expiration
#[arg(long)]
expires_at: Option,
+
+ /// due-at
+ #[arg(long)]
+ due_at: Option,
},
GetAccount {
/// id of account
@@ -465,8 +472,7 @@ pub enum CommandActions {
#[arg(long)]
account_id: String,
},
- GenerateUserOTP {
- },
+ GenerateUserOTP {},
GenerateAPIToken {
/// duration of token
#[arg(long)]
@@ -606,6 +612,7 @@ impl Args {
notes,
renew_interval_days,
expires_at,
+ due_at,
} => {
let account = self.build_account(
&vault_id,
@@ -626,6 +633,7 @@ impl Args {
notes,
renew_interval_days,
expires_at,
+ due_at,
);
Some(account)
}
@@ -648,7 +656,8 @@ impl Args {
icon,
notes,
renew_interval_days,
- expires_at, ..
+ expires_at,
+ due_at,
} => {
let mut account = self.build_account(
&vault_id,
@@ -669,6 +678,7 @@ impl Args {
notes,
renew_interval_days,
expires_at,
+ due_at,
);
account.details.account_id = account_id.clone();
Some(account)
@@ -747,6 +757,7 @@ impl Args {
notes: &Option,
renew_interval_days: &Option,
expires_at: &Option,
+ due_at: &Option,
) -> Account {
let kind = if let Some(kind) = kind {
kind.clone()
@@ -769,6 +780,7 @@ impl Args {
account.details.icon = icon.clone();
account.details.renew_interval_days = *renew_interval_days;
account.details.expires_at = safe_parse_string_date(expires_at.clone());
+ account.details.due_at = safe_parse_string_date(due_at.clone());
account.credentials.password = password.clone();
account.credentials.form_fields = HashMap::new();
diff --git a/src/domain/models.rs b/src/domain/models.rs
index cac046f..f3af4fe 100644
--- a/src/domain/models.rs
+++ b/src/domain/models.rs
@@ -774,7 +774,7 @@ impl AccountKind {
match self {
AccountKind::Logins => VaultKind::Logins,
AccountKind::Contacts => VaultKind::Contacts,
- AccountKind::Notes => VaultKind::SecureNotes,
+ AccountKind::Notes => VaultKind::Notes,
AccountKind::Custom => VaultKind::Custom,
}
}
@@ -869,6 +869,8 @@ pub struct AccountSummary {
pub renew_interval_days: Option,
// expiration
pub expires_at: Option,
+ // due-at
+ pub due_at: Option,
// The metadata for date when password was changed.
pub credentials_updated_at: Option,
// The metadata for date when password was analyzed.
@@ -909,6 +911,7 @@ impl AccountSummary {
advisories: HashMap::new(),
renew_interval_days: None,
expires_at: None,
+ due_at: None,
credentials_updated_at: None,
analyzed_at: None,
}
@@ -930,6 +933,9 @@ impl AccountSummary {
if lq.contains("favorite") && self.favorite {
return true;
}
+ if lq.contains("expire") && self.is_expired() || self.is_due() {
+ return true;
+ }
if lq.contains("high_risk") &&
(self.risk == AccountRisk::High || self.risk == AccountRisk::Medium) {
return true;
@@ -1010,6 +1016,28 @@ impl AccountSummary {
}
}
+ pub fn is_expired_or_overdue(&self) -> bool {
+ self.is_expired() || self.is_due()
+ }
+
+ pub fn is_expired(&self) -> bool {
+ if let Some(expires_at) = &self.expires_at {
+ if expires_at.timestamp_millis() < Utc::now().naive_utc().timestamp_millis() {
+ return true;
+ }
+ }
+ false
+ }
+
+ pub fn is_due(&self) -> bool {
+ if let Some(due_at) = &self.due_at {
+ if due_at.timestamp_millis() < Utc::now().naive_utc().timestamp_millis() {
+ return true;
+ }
+ }
+ false
+ }
+
pub fn favicon(&self) -> String {
if let Some(u) = &self.website_url {
if let Ok(mut u) = url::Url::parse(u) {
@@ -1027,6 +1055,19 @@ impl AccountSummary {
String::new()
}
+ pub fn expires_at(&self) -> String {
+ if let Some(expires_at) = &self.expires_at {
+ return expires_at.format("%Y-%m-%d").to_string();
+ }
+ String::from("")
+ }
+ pub fn due_at(&self) -> String {
+ if let Some(due_at) = &self.due_at {
+ return due_at.format("%Y-%m-%d").to_string();
+ }
+ String::from("")
+ }
+
pub fn email(&self) -> String {
self.email.clone().unwrap_or("".into())
}
@@ -1395,20 +1436,20 @@ impl AccountPasswordSummary {
pub const DEFAULT_VAULT_NAMES: [&str; 5] = ["Identity", "Personal", "Work", "Financial", "Secure Notes"];
pub const DEFAULT_CATEGORIES: [&str; 10] = [
- "Contacts",
"Logins",
+ "Contacts",
+ "Notes",
+ "Custom",
"Finance",
"Social",
"Shopping",
"Travel",
"Gaming",
- "Notes",
"Credit Cards",
- "Custom",
];
pub fn top_categories() -> Vec {
- DEFAULT_CATEGORIES[0..5].to_vec().iter().map(|s| s.to_string()).collect()
+ DEFAULT_CATEGORIES[0..3].to_vec().iter().map(|s| s.to_string()).collect()
}
pub fn all_categories() -> Vec {
@@ -1421,7 +1462,7 @@ pub fn all_categories() -> Vec {
pub enum VaultKind {
Logins,
Contacts,
- SecureNotes,
+ Notes,
Custom,
}
@@ -1430,11 +1471,11 @@ impl From<&str> for VaultKind {
match s {
"Logins" => VaultKind::Logins,
"Contacts" => VaultKind::Contacts,
- "SecureNotes" => VaultKind::SecureNotes,
+ "Notes" => VaultKind::Notes,
_ => {
let s = s.to_lowercase();
if s.contains("note") {
- VaultKind::SecureNotes
+ VaultKind::Notes
} else if s.contains("data") || s.contains("custom") {
VaultKind::Custom
} else {
@@ -1450,7 +1491,7 @@ impl Display for VaultKind {
match self {
VaultKind::Logins => write!(f, "Logins"),
VaultKind::Contacts => write!(f, "Contacts"),
- VaultKind::SecureNotes => write!(f, "SecureNotes"),
+ VaultKind::Notes => write!(f, "Notes"),
VaultKind::Custom => write!(f, "Custom"),
}
}
diff --git a/src/utils.rs b/src/utils.rs
index aa07dd5..ceb424f 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -27,6 +27,9 @@ pub fn safe_parse_str_date(d: &str) -> Option {
if let Ok(parsed) = NaiveDate::parse_from_str(d, "%Y-%m-%d") {
return Some(NaiveDateTime::new(parsed, NaiveTime::from_hms(0, 0, 0)));
}
+ if let Ok(parsed) = NaiveDate::parse_from_str(d, "%Y-%m-%d %H:%M:%S") {
+ return Some(NaiveDateTime::new(parsed, NaiveTime::from_hms(0, 0, 0)));
+ }
if let Ok(parsed) = NaiveDate::parse_from_str(d, "%Y/%m/%d") {
return Some(NaiveDateTime::new(parsed, NaiveTime::from_hms(0, 0, 0)));
}
diff --git a/templates/index.html b/templates/index.html
index af70763..985a253 100755
--- a/templates/index.html
+++ b/templates/index.html
@@ -72,6 +72,9 @@
High-Risk Accounts
+
+ Expired Accounts
+
{% for category in top_categories %}
{{category}}
@@ -84,7 +87,7 @@
@@ -114,7 +117,7 @@
Label |
Username |
- Email |
+ Expires/Due Date |
Category & Tags |
Action |
@@ -137,7 +140,11 @@
{% endif %}
{{account.username()}} |
-
{{account.email()}} |
+ {% if account.is_expired_or_overdue() %}
+
{{account.expires_at()}} {{account.due_at()}} |
+ {% else %}
+
{{account.expires_at()}} {{account.due_at()}} |
+ {% endif %}
{{account.all_cat_tags()}} |
|