Switch from lazy_static to once_cell

This commit is contained in:
Evie Viau-Chow-Stuart 2022-10-12 00:49:17 -04:00
parent 11e71bcbd1
commit 9c4efe027a
Signed by: evie
GPG key ID: 928652CDFCEC8099
5 changed files with 248 additions and 178 deletions

6
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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<String>,
password: Vec<String>
password: Vec<String>,
}
#[derive(Serialize)]
pub struct AuthUserFormResponse {
session_token: Option<String>,
issues: Option<AuthUserFormIssues>
issues: Option<AuthUserFormIssues>,
}
pub async fn login(
Extension(ref connection): Extension<DatabaseConnection>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
Form(input): Form<AuthUserForm>
Form(input): Form<AuthUserForm>,
) -> 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<Regex> = 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());
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) }));
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() {
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() {
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,
}),
),
}
}

View file

@ -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<String>,
email: Vec<String>,
password: Vec<String>
password: Vec<String>,
}
#[derive(Serialize)]
pub struct NewUserFormResponse {
session_token: Option<String>,
issues: Option<NewUserFormIssues>
issues: Option<NewUserFormIssues>,
}
pub async fn register(
Extension(ref connection): Extension<DatabaseConnection>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
Form(input): Form<NewUserForm>
Form(input): Form<NewUserForm>,
) -> 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<Regex> = 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());
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,34 +146,35 @@ 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 {
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)
permission: ActiveValue::Set(TeamPermissions::OWNER.to_string()),
})
.exec(connection)
.await;
if team_addition.is_err() {
@ -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() {
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() {
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 }))
}
}
}
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,
}),
),
};
}

View file

@ -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::Model> = 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)]
@ -82,7 +84,10 @@ impl<S> FromRequestParts<S> 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<S> FromRequestParts<S> for UserFromBearer
match split {
Some((name, contents)) if name == "Bearer" => {
// Get database connection from header
let connection: &DatabaseConnection = parts.extensions.get::<DatabaseConnection>()
let connection: &DatabaseConnection = parts
.extensions
.get::<DatabaseConnection>()
.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"))
None => Err((StatusCode::UNAUTHORIZED, "Provided token is invalid")),
Some(user) => Ok(Self(user)),
}
Some(user) => Ok(Self(user))
}
},
_ => Err((
StatusCode::BAD_REQUEST,
"`Authorization` header must be a bearer token",
@ -117,40 +122,35 @@ impl<S> FromRequestParts<S> 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<UserAgentParser> = 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
return session_name_builder;
}