diff --git a/src/api/admin.rs b/src/api/admin.rs index fe0405b3..1d6f5f72 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -18,7 +18,7 @@ use crate::{ db::{backup_database, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, - util::get_display_size, + util::{get_display_size, format_naive_datetime_local}, CONFIG, }; @@ -293,6 +293,7 @@ fn get_users_json(_token: AdminToken, conn: DbConn) -> JsonResult { #[get("/users/overview")] fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { let users = User::get_all(&conn); + let dt_fmt = "%Y-%m-%d %H:%M:%S %Z"; let users_json: Vec = users.iter() .map(|u| { let mut usr = u.to_json(&conn); @@ -300,9 +301,9 @@ fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult> { usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn)); usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn) as i32)); usr["user_enabled"] = json!(u.enabled); - usr["created_at"] = json!(&u.created_at.format("%Y-%m-%d %H:%M:%S").to_string()); + usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt)); usr["last_active"] = match u.last_active(&conn) { - Some(timestamp) => json!(timestamp.format("%Y-%m-%d %H:%M:%S").to_string()), + Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)), None => json!("Never") }; usr @@ -446,7 +447,7 @@ fn diagnostics(_token: AdminToken, _conn: DbConn) -> ApiResult> { // Run the date check as the last item right before filling the json. // This should ensure that the time difference between the browser and the server is as minimal as possible. let dt = Utc::now(); - let server_time = dt.format("%Y-%m-%d %H:%M:%S").to_string(); + let server_time = dt.format("%Y-%m-%d %H:%M:%S UTC").to_string(); let diagnostics_json = json!({ "dns_resolved": dns_resolved, diff --git a/src/mail.rs b/src/mail.rs index 7419169a..641d9127 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -1,7 +1,6 @@ -use std::{env, str::FromStr}; +use std::{str::FromStr}; use chrono::{DateTime, Local}; -use chrono_tz::Tz; use percent_encoding::{percent_encode, NON_ALPHANUMERIC}; use lettre::{ @@ -107,22 +106,6 @@ fn get_template(template_name: &str, data: &serde_json::Value) -> Result<(String Ok((subject, body)) } -pub fn format_datetime(dt: &DateTime) -> String { - let fmt = "%A, %B %_d, %Y at %r %Z"; - - // With a DateTime, `%Z` formats as the time zone's UTC offset - // (e.g., `+00:00`). If the `TZ` environment variable is set, try to - // format as a time zone abbreviation instead (e.g., `UTC`). - if let Ok(tz) = env::var("TZ") { - if let Ok(tz) = tz.parse::() { - return dt.with_timezone(&tz).format(fmt).to_string(); - } - } - - // Otherwise, fall back to just displaying the UTC offset. - dt.format(fmt).to_string() -} - pub fn send_password_hint(address: &str, hint: Option) -> EmptyResult { let template_name = if hint.is_some() { "email/pw_hint_some" @@ -257,13 +240,14 @@ pub fn send_new_device_logged_in(address: &str, ip: &str, dt: &DateTime, use crate::util::upcase_first; let device = upcase_first(device); + let fmt = "%A, %B %_d, %Y at %r %Z"; let (subject, body_html, body_text) = get_text( "email/new_device_logged_in", json!({ "url": CONFIG.domain(), "ip": ip, "device": device, - "datetime": format_datetime(dt), + "datetime": crate::util::format_datetime_local(dt, fmt), }), )?; diff --git a/src/static/templates/admin/diagnostics.hbs b/src/static/templates/admin/diagnostics.hbs index 72d7cab6..50d80f47 100644 --- a/src/static/templates/admin/diagnostics.hbs +++ b/src/static/templates/admin/diagnostics.hbs @@ -72,7 +72,7 @@ const hour = String(d.getUTCHours()).padStart(2, '0'); const minute = String(d.getUTCMinutes()).padStart(2, '0'); const seconds = String(d.getUTCSeconds()).padStart(2, '0'); - const browserUTC = year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + seconds; + const browserUTC = `${year}-${month}-${day} ${hour}:${minute}:${seconds} UTC`; document.getElementById("time-browser-string").innerText = browserUTC; const serverUTC = document.getElementById("time-server-string").innerText; @@ -147,4 +147,4 @@ } } })(); - \ No newline at end of file + diff --git a/src/util.rs b/src/util.rs index f33f5148..4b7b8614 100644 --- a/src/util.rs +++ b/src/util.rs @@ -322,12 +322,40 @@ pub fn get_env_bool(key: &str) -> Option { // Date util methods // -use chrono::NaiveDateTime; +use chrono::{DateTime, Local, NaiveDateTime, TimeZone}; +use chrono_tz::Tz; -const DATETIME_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.6fZ"; +/// Formats a UTC-offset `NaiveDateTime` in the format used by Bitwarden API +/// responses with "date" fields (`CreationDate`, `RevisionDate`, etc.). +pub fn format_date(dt: &NaiveDateTime) -> String { + dt.format("%Y-%m-%dT%H:%M:%S%.6fZ").to_string() +} + +/// Formats a `DateTime` using the specified format string. +/// +/// For a `DateTime`, the `%Z` specifier normally formats as the +/// time zone's UTC offset (e.g., `+00:00`). In this function, if the +/// `TZ` environment variable is set, then `%Z` instead formats as the +/// abbreviation for that time zone (e.g., `UTC`). +pub fn format_datetime_local(dt: &DateTime, fmt: &str) -> String { + // Try parsing the `TZ` environment variable to enable formatting `%Z` as + // a time zone abbreviation. + if let Ok(tz) = env::var("TZ") { + if let Ok(tz) = tz.parse::() { + return dt.with_timezone(&tz).format(fmt).to_string(); + } + } + + // Otherwise, fall back to formatting `%Z` as a UTC offset. + dt.format(fmt).to_string() +} -pub fn format_date(date: &NaiveDateTime) -> String { - date.format(DATETIME_FORMAT).to_string() +/// Formats a UTC-offset `NaiveDateTime` as a datetime in the local time zone. +/// +/// This function basically converts the `NaiveDateTime` to a `DateTime`, +/// and then calls [format_datetime_local](crate::util::format_datetime_local). +pub fn format_naive_datetime_local(dt: &NaiveDateTime, fmt: &str) -> String { + format_datetime_local(&Local.from_utc_datetime(dt), fmt) } //