diff --git a/Cargo.lock b/Cargo.lock index c03d581..f5ef06f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1050,9 +1050,9 @@ dependencies = [ "dotenv", "futures", "lapin", - "lazy_static", "log", "migration", + "once_cell", "picky", "pretty_env_logger", "rand", @@ -1859,9 +1859,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "onig" diff --git a/Cargo.toml b/Cargo.toml index 49ff867..4b20b68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ chrono = "0.4.22" zxcvbn = "2.2.1" -lazy_static = "1.4.0" +once_cell = "1.15.0" regex = "1.6.0" diff --git a/src/routes/users/login.rs b/src/routes/users/login.rs index 475e1e6..62bd05c 100644 --- a/src/routes/users/login.rs +++ b/src/routes/users/login.rs @@ -1,14 +1,14 @@ -use std::net::SocketAddr; use argon2::{Argon2, PasswordHash, PasswordVerifier}; -use axum::{Extension, Form, Json}; use axum::extract::ConnectInfo; use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; +use axum::{Extension, Form, Json}; use chrono::{Duration, NaiveDateTime}; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; use sea_orm::*; use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; use ulid::Ulid; use crate::entities::session; @@ -21,51 +21,63 @@ use crate::util::generate_session_token; #[derive(Deserialize)] pub struct AuthUserForm { email: String, - password: String + password: String, } #[derive(Serialize)] pub struct AuthUserFormIssues { email: Vec, - password: Vec + password: Vec, } #[derive(Serialize)] pub struct AuthUserFormResponse { session_token: Option, - issues: Option + issues: Option, } pub async fn login( Extension(ref connection): Extension, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, - Form(input): Form + Form(input): Form, ) -> impl IntoResponse { let mut validation_issues = AuthUserFormIssues { email: vec![], - password: vec![] + password: vec![], }; - lazy_static! { - static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#) - .expect("Failed to compile email regex"); - } + static EMAIL_RE: Lazy = Lazy::new(|| { + Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#) + .expect("Failed to compile email regex") + }); // Ensure form contents are safe if input.email.is_empty() { - validation_issues.email.push("Email cannot be empty.".to_string()); + validation_issues + .email + .push("Email cannot be empty.".to_string()); } else if !EMAIL_RE.is_match(&input.email) { - validation_issues.email.push("Email is invalid.".to_string()); + validation_issues + .email + .push("Email is invalid.".to_string()); } - if input.password.is_empty(){ - validation_issues.password.push("Password cannot be empty.".to_string()); + if input.password.is_empty() { + validation_issues + .password + .push("Password cannot be empty.".to_string()); } // Return early if we have issues with form content so far - if !validation_issues.email.is_empty() || !validation_issues.password.is_empty() { - return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) })); + if !validation_issues.email.is_empty() || !validation_issues.password.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(AuthUserFormResponse { + session_token: None, + issues: Some(validation_issues), + }), + ); } // Check to see if a user with the email exists @@ -76,9 +88,17 @@ pub async fn login( .expect("Failed to check database."); if !existing_user.is_some() { - validation_issues.email.push("An account with this email doesn't exist.".to_string()); + validation_issues + .email + .push("An account with this email doesn't exist.".to_string()); - return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) })); + return ( + StatusCode::BAD_REQUEST, + Json(AuthUserFormResponse { + session_token: None, + issues: Some(validation_issues), + }), + ); } let existing_user = existing_user.unwrap(); @@ -86,9 +106,20 @@ pub async fn login( // Check password let existing_password_hash = PasswordHash::new(&existing_user.password) .expect("Failed to generate password hash from database."); - if !Argon2::default().verify_password(input.password.as_bytes(), &existing_password_hash).is_ok() { - validation_issues.password.push("Incorrect password.".to_string()); - return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) })); + if !Argon2::default() + .verify_password(input.password.as_bytes(), &existing_password_hash) + .is_ok() + { + validation_issues + .password + .push("Incorrect password.".to_string()); + return ( + StatusCode::BAD_REQUEST, + Json(AuthUserFormResponse { + session_token: None, + issues: Some(validation_issues), + }), + ); } let session_name; @@ -96,16 +127,14 @@ pub async fn login( None => { session_name = String::from("Unknown"); } - Some(header) => { - match header.to_str() { - Ok(header) => { - session_name = assemble_session_name(header).await; - } - Err(_) => { - session_name = String::from("Unknown"); - } + Some(header) => match header.to_str() { + Ok(header) => { + session_name = assemble_session_name(header).await; } - } + Err(_) => { + session_name = String::from("Unknown"); + } + }, } let ip; @@ -113,16 +142,14 @@ pub async fn login( None => { ip = addr.ip().to_string(); } - Some(header) => { - match header.to_str() { - Ok(header) => { - ip = String::from(header); - } - Err(_) => { - ip = addr.ip().to_string(); - } + Some(header) => match header.to_str() { + Ok(header) => { + ip = String::from(header); } - } + Err(_) => { + ip = addr.ip().to_string(); + } + }, } let session_token = generate_session_token(); @@ -135,19 +162,25 @@ pub async fn login( ip: ActiveValue::set(ip.clone()), token: ActiveValue::Set(session_token.clone()), context: ActiveValue::Set(existing_user.id.clone()), - expiry: ActiveValue::Set(expiry) + expiry: ActiveValue::Set(expiry), }; - let session_res = Session::insert(new_session) - .exec(connection) - .await; + let session_res = Session::insert(new_session).exec(connection).await; match session_res { - Ok(_) => { - (StatusCode::OK, Json(AuthUserFormResponse { session_token: Some(session_token), issues: None })) - } - Err(_) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(AuthUserFormResponse { session_token: None, issues: None })) - } + Ok(_) => ( + StatusCode::OK, + Json(AuthUserFormResponse { + session_token: Some(session_token), + issues: None, + }), + ), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(AuthUserFormResponse { + session_token: None, + issues: None, + }), + ), } -} \ No newline at end of file +} diff --git a/src/routes/users/register.rs b/src/routes/users/register.rs index 598b932..1f327e2 100644 --- a/src/routes/users/register.rs +++ b/src/routes/users/register.rs @@ -1,81 +1,89 @@ -use std::net::SocketAddr; -use axum::{Extension, Form, Json}; use axum::extract::ConnectInfo; use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; +use axum::{Extension, Form, Json}; use chrono::{Duration, NaiveDateTime}; +use once_cell::sync::Lazy; +use regex::Regex; use sea_orm::*; use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; use ulid::Ulid; -use lazy_static::lazy_static; -use regex::Regex; -use crate::entities::{team, team_member, user}; use crate::entities::prelude::{Team, TeamMember}; use crate::entities::user::Entity as User; +use crate::entities::{team, team_member, user}; use crate::entities::session; use crate::entities::session::Entity as Session; -use crate::util::{generate_session_token, hash_password}; use crate::util::auth::{assemble_session_name, TeamPermissions}; +use crate::util::{generate_session_token, hash_password}; #[derive(Deserialize, Clone)] pub struct NewUserForm { name: String, email: String, - password: String + password: String, } #[derive(Serialize)] pub struct NewUserFormIssues { name: Vec, email: Vec, - password: Vec + password: Vec, } #[derive(Serialize)] pub struct NewUserFormResponse { session_token: Option, - issues: Option + issues: Option, } pub async fn register( Extension(ref connection): Extension, ConnectInfo(addr): ConnectInfo, headers: HeaderMap, - Form(input): Form + Form(input): Form, ) -> impl IntoResponse { let mut validation_issues = NewUserFormIssues { name: vec![], email: vec![], - password: vec![] + password: vec![], }; - lazy_static! { - static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#) - .expect("Failed to compile email regex"); - } + static EMAIL_RE: Lazy = Lazy::new(|| { + Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#) + .expect("Failed to compile email regex") + }); // Ensure form contents are safe if input.name.is_empty() { - validation_issues.name.push("Name cannot be empty.".to_string()); + validation_issues + .name + .push("Name cannot be empty.".to_string()); } else if input.name.len() > 128 { validation_issues.name.push("Name is too long.".to_string()); } if input.email.is_empty() { - validation_issues.email.push("Email cannot be empty.".to_string()); + validation_issues + .email + .push("Email cannot be empty.".to_string()); } else if !EMAIL_RE.is_match(&input.email) { - validation_issues.email.push("Email is invalid.".to_string()); + validation_issues + .email + .push("Email is invalid.".to_string()); } - if input.password.is_empty(){ - validation_issues.password.push("Password cannot be empty.".to_string()); + if input.password.is_empty() { + validation_issues + .password + .push("Password cannot be empty.".to_string()); } else { // Check password security - let pass_estimate = zxcvbn::zxcvbn(&input.password, &[]) - .expect("Failed to check password security."); + let pass_estimate = + zxcvbn::zxcvbn(&input.password, &[]).expect("Failed to check password security."); if pass_estimate.score() <= 2 { for i in pass_estimate.feedback().clone().unwrap().suggestions() { @@ -85,8 +93,17 @@ pub async fn register( } // Return early if we have issues with form content so far - if !validation_issues.name.is_empty() || !validation_issues.email.is_empty() || !validation_issues.password.is_empty() { - return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) })); + if !validation_issues.name.is_empty() + || !validation_issues.email.is_empty() + || !validation_issues.password.is_empty() + { + return ( + StatusCode::BAD_REQUEST, + Json(NewUserFormResponse { + session_token: None, + issues: Some(validation_issues), + }), + ); } // Check to see if a user with the email already exists @@ -97,9 +114,17 @@ pub async fn register( .expect("Failed to check database."); if existing_user.is_some() { - validation_issues.email.push("An account with this email already exists.".to_string()); + validation_issues + .email + .push("An account with this email already exists.".to_string()); - return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) })); + return ( + StatusCode::BAD_REQUEST, + Json(NewUserFormResponse { + session_token: None, + issues: Some(validation_issues), + }), + ); } let user_id = Ulid::new().to_string(); @@ -112,9 +137,7 @@ pub async fn register( ..Default::default() }; - let res = User::insert(new_user.clone()) - .exec(connection) - .await; + let res = User::insert(new_user.clone()).exec(connection).await; return match res { Ok(_) => { @@ -123,35 +146,36 @@ pub async fn register( let new_team = team::ActiveModel { id: ActiveValue::set(team_id.clone()), - name: ActiveValue::Set( - format!("{}'s Personal Team", &input.name) - ), + name: ActiveValue::Set(format!("{}'s Personal Team", &input.name)), active: Default::default(), - personal: ActiveValue::Set(true) + personal: ActiveValue::Set(true), }; - let team_creation = Team::insert(new_team.clone()) - .exec(connection) - .await; + let team_creation = Team::insert(new_team.clone()).exec(connection).await; if team_creation.is_err() { // Delete user on team creation error new_user.delete(connection).await .expect("Failed to delete user from database after failing to create personal team for said user!"); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None })); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(NewUserFormResponse { + session_token: None, + issues: None, + }), + ); } // Add user to personal team - let team_addition = TeamMember::insert( - team_member::ActiveModel { - id: ActiveValue::Set(String::from(Ulid::new())), - team_id: ActiveValue::Set(team_id.clone()), - user_id: ActiveValue::Set(user_id.clone()), - permission: ActiveValue::Set(TeamPermissions::OWNER.to_string()) - } - ).exec(connection) - .await; + let team_addition = TeamMember::insert(team_member::ActiveModel { + id: ActiveValue::Set(String::from(Ulid::new())), + team_id: ActiveValue::Set(team_id.clone()), + user_id: ActiveValue::Set(user_id.clone()), + permission: ActiveValue::Set(TeamPermissions::OWNER.to_string()), + }) + .exec(connection) + .await; if team_addition.is_err() { // Delete team and user on permission addition error @@ -160,7 +184,13 @@ pub async fn register( new_user.delete(connection).await .expect("Failed to delete user from database after failing to add permissions for personal team for said user!"); - return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None })); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(NewUserFormResponse { + session_token: None, + issues: None, + }), + ); } let session_name; @@ -168,16 +198,14 @@ pub async fn register( None => { session_name = String::from("Unknown"); } - Some(header) => { - match header.to_str() { - Ok(header) => { - session_name = assemble_session_name(header).await; - } - Err(_) => { - session_name = String::from("Unknown"); - } + Some(header) => match header.to_str() { + Ok(header) => { + session_name = assemble_session_name(header).await; } - } + Err(_) => { + session_name = String::from("Unknown"); + } + }, } let ip; @@ -185,20 +213,19 @@ pub async fn register( None => { ip = addr.ip().to_string(); } - Some(header) => { - match header.to_str() { - Ok(header) => { - ip = String::from(header); - } - Err(_) => { - ip = addr.ip().to_string(); - } + Some(header) => match header.to_str() { + Ok(header) => { + ip = String::from(header); } - } + Err(_) => { + ip = addr.ip().to_string(); + } + }, } let session_token = generate_session_token(); - let expiry: NaiveDateTime = chrono::offset::Utc::now().naive_local() + Duration::days(20); + let expiry: NaiveDateTime = + chrono::offset::Utc::now().naive_local() + Duration::days(20); // Generate session for newly created user let new_session = session::ActiveModel { @@ -207,24 +234,34 @@ pub async fn register( ip: ActiveValue::set(ip.clone()), token: ActiveValue::Set(session_token.clone()), context: ActiveValue::Set(user_id.clone()), - expiry: ActiveValue::Set(expiry) + expiry: ActiveValue::Set(expiry), }; - let session_res = Session::insert(new_session) - .exec(connection) - .await; + let session_res = Session::insert(new_session).exec(connection).await; match session_res { - Ok(_) => { - (StatusCode::CREATED, Json(NewUserFormResponse { session_token: Some(session_token), issues: None })) - } - Err(_) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None })) - } + Ok(_) => ( + StatusCode::CREATED, + Json(NewUserFormResponse { + session_token: Some(session_token), + issues: None, + }), + ), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(NewUserFormResponse { + session_token: None, + issues: None, + }), + ), } } - Err(_) => { - (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None })) - } - } -} \ No newline at end of file + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(NewUserFormResponse { + session_token: None, + issues: None, + }), + ), + }; +} diff --git a/src/util/auth.rs b/src/util/auth.rs index cf29232..3bebe40 100644 --- a/src/util/auth.rs +++ b/src/util/auth.rs @@ -5,21 +5,21 @@ use std::fmt::Formatter; use async_trait::async_trait; use axum::extract::FromRequestParts; -use axum::http::{header::AUTHORIZATION, StatusCode}; use axum::http::request::Parts; -use lazy_static::lazy_static; +use axum::http::{header::AUTHORIZATION, StatusCode}; +use once_cell::sync::Lazy; use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; -use user_agent_parser::{OS, Product}; use user_agent_parser::UserAgentParser; +use user_agent_parser::{Product, OS}; -use crate::entities::{session, user}; use crate::entities::prelude::{Session, User}; +use crate::entities::{session, user}; pub enum TeamPermissions { OWNER, ADMIN, EDITOR, - VIEWER + VIEWER, } impl fmt::Display for TeamPermissions { @@ -28,13 +28,16 @@ impl fmt::Display for TeamPermissions { TeamPermissions::OWNER => write!(f, "OWNER"), TeamPermissions::ADMIN => write!(f, "ADMIN"), TeamPermissions::EDITOR => write!(f, "EDITOR"), - TeamPermissions::VIEWER => write!(f, "VIEWER") + TeamPermissions::VIEWER => write!(f, "VIEWER"), } } } /// Gets a user model and session id from a supplied session token -pub async fn get_user_from_token(token: String, connection: &DatabaseConnection) -> Option<(user::Model, String)> { +pub async fn get_user_from_token( + token: String, + connection: &DatabaseConnection, +) -> Option<(user::Model, String)> { let requested_session: Option = Session::find() .filter(session::Column::Token.eq(token)) .one(connection) @@ -42,9 +45,7 @@ pub async fn get_user_from_token(token: String, connection: &DatabaseConnection) .expect("Failed to retrieve session from the database."); return match requested_session { - None => { - None - } + None => None, Some(_) => { let requested_session = requested_session.unwrap(); @@ -56,15 +57,16 @@ pub async fn get_user_from_token(token: String, connection: &DatabaseConnection) match contexted_user { None => { - error!("Session {} still exists for user {} of which doesn't exist!", requested_session.id, requested_session.context); + error!( + "Session {} still exists for user {} of which doesn't exist!", + requested_session.id, requested_session.context + ); None } - Some(_) => { - Some((contexted_user.unwrap(), requested_session.id)) - } + Some(_) => Some((contexted_user.unwrap(), requested_session.id)), } } - } + }; } #[derive(Clone)] @@ -72,8 +74,8 @@ pub struct UserFromBearer(pub (user::Model, String)); #[async_trait] impl FromRequestParts for UserFromBearer - where - S: Send + Sync, +where + S: Send + Sync, { type Rejection = (StatusCode, &'static str); @@ -82,7 +84,10 @@ impl FromRequestParts for UserFromBearer let authorisation = parts .headers .get(AUTHORIZATION) - .ok_or((StatusCode::UNAUTHORIZED, "`Authorization` header is missing"))? + .ok_or(( + StatusCode::UNAUTHORIZED, + "`Authorization` header is missing", + ))? .to_str() .map_err(|_| { ( @@ -96,16 +101,16 @@ impl FromRequestParts for UserFromBearer match split { Some((name, contents)) if name == "Bearer" => { // Get database connection from header - let connection: &DatabaseConnection = parts.extensions.get::() + let connection: &DatabaseConnection = parts + .extensions + .get::() .expect("Failed to get database connection from users extractor"); match get_user_from_token(contents.to_string(), connection).await { - None => { - Err((StatusCode::UNAUTHORIZED, "Provided token is invalid")) - } - Some(user) => Ok(Self(user)) + None => Err((StatusCode::UNAUTHORIZED, "Provided token is invalid")), + Some(user) => Ok(Self(user)), } - }, + } _ => Err(( StatusCode::BAD_REQUEST, "`Authorization` header must be a bearer token", @@ -117,40 +122,35 @@ impl FromRequestParts for UserFromBearer pub async fn assemble_session_name(header: &str) -> String { // TODO: Maybe reconsider having a static session name? - lazy_static! { - static ref UAP: UserAgentParser = UserAgentParser::from_path(&env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml"))).expect("Failed to load regexes.yaml"); - } + static UAP: Lazy = Lazy::new(|| { + UserAgentParser::from_path( + &env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml")), + ) + .expect("Failed to load regexes.yaml") + }); let product: Product = UAP.parse_product(&header); let os: OS = UAP.parse_os(&header); let mut session_name_builder: String = String::new(); - session_name_builder.push_str( - &product.name.unwrap_or(Cow::from("Unknown")) - ); + session_name_builder.push_str(&product.name.unwrap_or(Cow::from("Unknown"))); if product.major.is_some() { session_name_builder.push_str(" "); - session_name_builder.push_str( - &product.major.unwrap() - ) + session_name_builder.push_str(&product.major.unwrap()) } session_name_builder.push_str(" / "); - session_name_builder.push_str( - &os.name.unwrap_or(Cow::from("Unknown")) - ); + session_name_builder.push_str(&os.name.unwrap_or(Cow::from("Unknown"))); if os.major.is_some() { session_name_builder.push_str(" "); - session_name_builder.push_str( - &os.major.unwrap() - ) + session_name_builder.push_str(&os.major.unwrap()) }; - return session_name_builder -} \ No newline at end of file + return session_name_builder; +}