From d7a55f6579b119fcb081c5dfeb747b4237bf17c5 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Tue, 24 Dec 2024 13:17:01 +0100 Subject: [PATCH 01/83] add session creation ts --- src/auth/auth.sql | 15 +++++++++++++++ src/auth/mod.rs | 3 +++ src/auth/user.rs | 11 ++++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/auth/auth.sql diff --git a/src/auth/auth.sql b/src/auth/auth.sql new file mode 100644 index 0000000..fb95da5 --- /dev/null +++ b/src/auth/auth.sql @@ -0,0 +1,15 @@ +CREATE TYPE user_role AS ENUM ('regular', 'admin'); + +CREATE TABLE IF NOT EXISTS users ( + username VARCHAR(255) NOT NULL PRIMARY KEY, + "password" text NOT NULL, + user_role user_role NOT NULL DEFAULT 'regular' +); + +CREATE TABLE IF NOT EXISTS user_session ( + id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), + token text NOT NULL, + "user" varchar(255) NOT NULL, + "created" timestamptz NOT NULL DEFAULT NOW(), + FOREIGN KEY("user") REFERENCES users(username) +); \ No newline at end of file diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 82585bb..74d429a 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,3 +1,4 @@ +use chrono::Utc; use data_encoding::HEXUPPER; use rand::RngCore; use serde::{Deserialize, Serialize}; @@ -25,6 +26,8 @@ pub struct Session { pub token: String, /// The username associated with the session token pub user: String, + /// Session creation time + pub created: chrono::DateTime } /// A macro to check if a user has admin privileges. diff --git a/src/auth/user.rs b/src/auth/user.rs index ad85fb0..0b960b5 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -26,6 +26,8 @@ pub struct User { pub password: String, /// The role of the user pub user_role: UserRole, + #[sqlx(default)] + pub session: String } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] @@ -40,7 +42,14 @@ pub enum UserRole { impl User { // Get a user from session ID pub async fn from_session(session: &str) -> Option { - sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(session).fetch_optional(get_pg!()).await.unwrap() + let user: Option = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(session).fetch_optional(get_pg!()).await.unwrap(); + + if let Some(mut user) = user { + user.session = session.to_string(); + return Some(user); + } + + None } /// Find a user by their username From e5fe40e4bed87f9a719fbf1fb0ad4a0bdb0d5934 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 27 Dec 2024 03:56:27 +0100 Subject: [PATCH 02/83] user update --- README.md | 19 +------ src/auth/auth.sql | 9 ++++ src/auth/csrf.rs | 36 +++++++++++++ src/auth/mod.rs | 20 ++----- src/auth/profile_pic.rs | 31 +++++++++++ src/auth/session.rs | 116 ++++++++++++++++++++++++++++++++++++++++ src/auth/user.rs | 45 +++------------- src/lib.rs | 6 +-- 8 files changed, 205 insertions(+), 77 deletions(-) create mode 100644 src/auth/csrf.rs create mode 100644 src/auth/profile_pic.rs create mode 100644 src/auth/session.rs diff --git a/README.md b/README.md index 994c265..512c039 100644 --- a/README.md +++ b/README.md @@ -9,24 +9,7 @@ Based is a micro framework providing web dev primitives. - Templates (Shell) ## User Auth -To use the user auth feature, make sure a migration has added the following to your PostgresDB: - -```sql -CREATE TYPE user_role AS ENUM ('regular', 'admin'); - -CREATE TABLE IF NOT EXISTS users ( - username VARCHAR(255) NOT NULL PRIMARY KEY, - "password" text NOT NULL, - user_role user_role NOT NULL DEFAULT 'regular' -); - -CREATE TABLE IF NOT EXISTS user_session ( - id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(), - token text NOT NULL, - "user" varchar(255) NOT NULL, - FOREIGN KEY("user") REFERENCES users(username) -); -``` +To use the user auth feature, make sure a migration has added [these tables](src/auth/auth.sql) to your PostgresDB: ## HTMX Based has a route for serving HTMX at `based::htmx::htmx_script_route` which you can include in your rocket `routes!`. The HTMX script will be available at `/assets/htmx.min.js`. diff --git a/src/auth/auth.sql b/src/auth/auth.sql index fb95da5..704a007 100644 --- a/src/auth/auth.sql +++ b/src/auth/auth.sql @@ -1,4 +1,5 @@ CREATE TYPE user_role AS ENUM ('regular', 'admin'); +CREATE TYPE session_kind AS ENUM ('api', 'user'); CREATE TABLE IF NOT EXISTS users ( username VARCHAR(255) NOT NULL PRIMARY KEY, @@ -11,5 +12,13 @@ CREATE TABLE IF NOT EXISTS user_session ( token text NOT NULL, "user" varchar(255) NOT NULL, "created" timestamptz NOT NULL DEFAULT NOW(), + "csrf" UUID NOT NULL DEFAULT gen_random_uuid(), + "name" varchar(255) + kind session_kind NOT NULL DEFAULT 'user', FOREIGN KEY("user") REFERENCES users(username) +); + +CREATE TABLE IF NOT EXISTS user_profile_pic ( + username VARCHAR(255) NOT NULL PRIMARY KEY, + "image" bytea NOT NULL ); \ No newline at end of file diff --git a/src/auth/csrf.rs b/src/auth/csrf.rs new file mode 100644 index 0000000..74cb84d --- /dev/null +++ b/src/auth/csrf.rs @@ -0,0 +1,36 @@ +use super::User; +use crate::get_pg; +use std::str::FromStr; + +pub trait CSRF { + fn get_csrf(&self) -> impl std::future::Future; + fn verify_csrf(&self, csrf: &str) -> impl std::future::Future; +} + +impl CSRF for User { + /// Get CSRF Token for the current session + async fn get_csrf(&self) -> uuid::Uuid { + let res: (uuid::Uuid,) = sqlx::query_as("SELECT csrf FROM user_session WHERE token = $1") + .bind(&self.session) + .fetch_one(get_pg!()) + .await + .unwrap(); + + res.0 + } + + /// Verify CSRF and generate a new one + async fn verify_csrf(&self, csrf: &str) -> bool { + if self.get_csrf().await == uuid::Uuid::from_str(csrf).unwrap() { + sqlx::query("UPDATE user_session SET csrf = gen_random_uuid() WHERE token = $1") + .bind(&self.session) + .execute(get_pg!()) + .await + .unwrap(); + + return true; + } + + false + } +} diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 74d429a..76067d7 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,10 +1,12 @@ -use chrono::Utc; use data_encoding::HEXUPPER; use rand::RngCore; -use serde::{Deserialize, Serialize}; -use sqlx::FromRow; +pub mod csrf; +pub mod profile_pic; +mod session; mod user; +pub use session::Session; +pub use session::Sessions; pub use user::AdminUser; pub use user::MaybeUser; pub use user::User; @@ -18,18 +20,6 @@ fn gen_token(token_length: usize) -> String { HEXUPPER.encode(&token_bytes) } -#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] -pub struct Session { - /// The unique ID of the session token - pub id: uuid::Uuid, - /// The generated session token - pub token: String, - /// The username associated with the session token - pub user: String, - /// Session creation time - pub created: chrono::DateTime -} - /// A macro to check if a user has admin privileges. /// /// This macro checks whether the provided user has admin privileges by calling the `is_admin` method on it. diff --git a/src/auth/profile_pic.rs b/src/auth/profile_pic.rs new file mode 100644 index 0000000..71b4d5c --- /dev/null +++ b/src/auth/profile_pic.rs @@ -0,0 +1,31 @@ +use crate::get_pg; + +use super::User; +pub trait ProfilePic { + fn profile_pic(&self) -> impl std::future::Future>>; + fn set_profile_pic(&self, image: Vec) -> impl std::future::Future; +} + +impl ProfilePic for User { + /// Get a user's profile picture from the database + async fn profile_pic(&self) -> Option> { + let res: Option<(Vec,)> = + sqlx::query_as("SELECT image FROM user_profile_pic WHERE username = $1;") + .bind(&self.username) + .fetch_optional(get_pg!()) + .await + .unwrap(); + + res.map(|x| x.0) + } + + /// Set a user's profile picture in the database + async fn set_profile_pic(&self, image: Vec) { + sqlx::query("INSERT INTO user_profile_pic (username, image) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET image = EXCLUDED.image", + ) + .bind(&self.username) + .bind(&image) + .execute(get_pg!()) + .await.unwrap(); + } +} diff --git a/src/auth/session.rs b/src/auth/session.rs new file mode 100644 index 0000000..f7fa004 --- /dev/null +++ b/src/auth/session.rs @@ -0,0 +1,116 @@ +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use sqlx::FromRow; + +use crate::get_pg; + +use super::{User, UserRole, gen_token}; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct Session { + /// The unique ID of the session token + pub id: uuid::Uuid, + /// The generated session token + pub token: String, + /// The username associated with the session token + pub user: String, + /// Session creation time + pub created: chrono::DateTime, + /// Internal CSRF value + csrf: uuid::Uuid, + /// Named session value + pub name: Option, + /// Kind of session + pub kind: SessionKind, +} + +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] +#[sqlx(type_name = "session_kind", rename_all = "lowercase")] +pub enum SessionKind { + API, + USER, +} + +pub trait Sessions { + fn from_session(session: &str) -> impl std::future::Future>; + fn login( + username: &str, + password: &str, + ) -> impl std::future::Future>; + fn api_key(&self, name: &str) -> impl std::future::Future; + fn session(&self) -> impl std::future::Future; + fn list_sessions(&self) -> impl std::future::Future>; + fn end_session(&self) -> impl std::future::Future; +} + +impl Sessions for User { + /// Generate a new API Key session + async fn api_key(&self, name: &str) -> Session { + sqlx::query_as( + "INSERT INTO user_session (token, \"user\", kind, name) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(gen_token(64)) + .bind(&self.username) + .bind(SessionKind::API) + .bind(name) + .fetch_one(get_pg!()) + .await + .unwrap() + } + + /// End a user session + async fn end_session(&self) -> () { + sqlx::query("DELETE FROM user_session WHERE token = $1") + .bind(&self.session) + .execute(get_pg!()) + .await + .unwrap(); + } + + /// Get all sessions for a user + async fn list_sessions(&self) -> Vec { + sqlx::query_as("SELECT * FROM user_session WHERE user = $1") + .bind(&self.username) + .fetch_all(get_pg!()) + .await + .unwrap() + } + + // Get a user from session ID + async fn from_session(session: &str) -> Option { + let user: Option = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(session).fetch_optional(get_pg!()).await.unwrap(); + + if let Some(mut user) = user { + user.session = session.to_string(); + return Some(user); + } + + None + } + + /// Login a user with the given username and password + async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> { + let u = Self::find(username).await?; + + if !u.verify_pw(password) { + return None; + } + + Some((u.session().await, u.user_role)) + } + + /// Generate a new session token for the user + /// + /// Returns a Session instance containing the generated token and associated user + async fn session(&self) -> Session { + sqlx::query_as( + "INSERT INTO user_session (token, \"user\", kind) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(gen_token(64)) + .bind(&self.username) + .bind(SessionKind::USER) + .fetch_one(get_pg!()) + .await + .unwrap() + } +} diff --git a/src/auth/user.rs b/src/auth/user.rs index 0b960b5..a710037 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -3,9 +3,11 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::FromRow; -use super::{Session, gen_token}; +use super::Sessions; use crate::{get_pg, request::api::ToAPI}; +// TODO : 2FA + /// User /// /// # Example: @@ -27,7 +29,7 @@ pub struct User { /// The role of the user pub user_role: UserRole, #[sqlx(default)] - pub session: String + pub session: String, } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] @@ -40,18 +42,6 @@ pub enum UserRole { } impl User { - // Get a user from session ID - pub async fn from_session(session: &str) -> Option { - let user: Option = sqlx::query_as("SELECT * FROM users WHERE username = (SELECT \"user\" FROM user_session WHERE token = $1)").bind(session).fetch_optional(get_pg!()).await.unwrap(); - - if let Some(mut user) = user { - user.session = session.to_string(); - return Some(user); - } - - None - } - /// Find a user by their username pub async fn find(username: &str) -> Option { sqlx::query_as("SELECT * FROM users WHERE username = $1") @@ -74,6 +64,7 @@ impl User { username: username.to_string(), password: bcrypt::hash(password, bcrypt::DEFAULT_COST).unwrap(), user_role: role, + session: String::new(), }; sqlx::query("INSERT INTO users (username, \"password\", user_role) VALUES ($1, $2, $3)") @@ -87,17 +78,6 @@ impl User { Some(u) } - /// Login a user with the given username and password - pub async fn login(username: &str, password: &str) -> Option<(Session, UserRole)> { - let u = Self::find(username).await?; - - if !u.verify_pw(password) { - return None; - } - - Some((u.session().await, u.user_role)) - } - /// Change the password of a User /// /// Returns a Result indicating whether the password change was successful or not @@ -124,20 +104,6 @@ impl User { .unwrap() } - /// Generate a new session token for the user - /// - /// Returns a Session instance containing the generated token and associated user - pub async fn session(&self) -> Session { - sqlx::query_as( - "INSERT INTO user_session (token, \"user\") VALUES ($1, $2) RETURNING id, token, \"user\"", - ) - .bind(gen_token(64)) - .bind(&self.username) - .fetch_one(get_pg!()) - .await - .unwrap() - } - /// Check if the user is an admin pub const fn is_admin(&self) -> bool { matches!(self.user_role, UserRole::Admin) @@ -160,6 +126,7 @@ impl ToAPI for User { } } +/// extracts a user from a request with `session` cookie async fn extract_user<'r>(request: &'r Request<'_>) -> Option { if let Some(session_id) = request.cookies().get("session") { if let Some(user) = User::from_session(session_id.value()).await { diff --git a/src/lib.rs b/src/lib.rs index 10dda8e..8acc857 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,17 +2,13 @@ use tokio::sync::OnceCell; pub mod auth; pub mod format; +pub mod htmx; pub mod page; pub mod request; pub mod result; -pub mod htmx; -// TODO : Cache Headers -// TODO : Refactor Responders -// TODO : Streaming Responses + Ranges // TODO : API Pagination? // TODO : CORS? -// TODO : CSRF? // Postgres From 7d6d301e362dc95ec205b7c0703aecc14b37652d Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 27 Dec 2024 04:10:35 +0100 Subject: [PATCH 03/83] fix --- src/auth/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 76067d7..e6b7342 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -7,6 +7,7 @@ mod session; mod user; pub use session::Session; pub use session::Sessions; +pub use user::APIUser; pub use user::AdminUser; pub use user::MaybeUser; pub use user::User; From 04852f2fbcc301d0c2b4098f613b9450b4474363 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 27 Dec 2024 04:12:09 +0100 Subject: [PATCH 04/83] fix --- src/auth/user.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/auth/user.rs b/src/auth/user.rs index a710037..f5a5a00 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -153,7 +153,7 @@ impl<'r> FromRequest<'r> for User { } /// Struct which extracts a user with session from `Token` HTTP Header. -pub struct APIUser(User); +pub struct APIUser(pub User); #[rocket::async_trait] impl<'r> FromRequest<'r> for APIUser { @@ -244,7 +244,7 @@ impl MaybeUser { /// ... /// } /// ``` -pub struct AdminUser(User); +pub struct AdminUser(pub User); #[rocket::async_trait] impl<'r> FromRequest<'r> for AdminUser { From 38373021611149d2ebc6d33a269375ec240527cb Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 29 Dec 2024 20:03:04 +0100 Subject: [PATCH 05/83] update --- src/auth/auth.sql | 2 +- src/request/context.rs | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/auth/auth.sql b/src/auth/auth.sql index 704a007..22cc046 100644 --- a/src/auth/auth.sql +++ b/src/auth/auth.sql @@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS user_session ( "user" varchar(255) NOT NULL, "created" timestamptz NOT NULL DEFAULT NOW(), "csrf" UUID NOT NULL DEFAULT gen_random_uuid(), - "name" varchar(255) + "name" varchar(255), kind session_kind NOT NULL DEFAULT 'user', FOREIGN KEY("user") REFERENCES users(username) ); diff --git a/src/request/context.rs b/src/request/context.rs index dc1e3f6..acdec30 100644 --- a/src/request/context.rs +++ b/src/request/context.rs @@ -25,3 +25,9 @@ impl<'r> FromRequest<'r> for RequestContext { }) } } + +impl Default for RequestContext { + fn default() -> Self { + Self { is_htmx: false } + } +} From 439467f7303c674f79da119aeef0e220b94bb8ff Mon Sep 17 00:00:00 2001 From: JMARyA Date: Sun, 29 Dec 2024 20:39:10 +0100 Subject: [PATCH 06/83] refactor --- build.rs | 4 ++-- src/auth/session.rs | 3 ++- src/auth/user.rs | 25 ++++++++++++++----------- src/format.rs | 7 +++++-- src/lib.rs | 2 +- src/page/mod.rs | 12 ++++++++---- src/request/api.rs | 2 ++ src/request/assets.rs | 10 +++++----- src/request/context.rs | 6 +----- src/request/mod.rs | 6 +++++- 10 files changed, 45 insertions(+), 32 deletions(-) diff --git a/build.rs b/build.rs index c1dd6fd..24382b4 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,7 @@ fn main() { let url = "https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js"; let dest_path = Path::new("src/htmx.min.js"); - println!("Downloading htmx.min.js from {}", url); + println!("Downloading htmx.min.js from {url}"); let response = reqwest::blocking::get(url) .expect("Failed to send HTTP request") .error_for_status() @@ -13,7 +13,7 @@ fn main() { let content = response.bytes().expect("Failed to read response body"); - fs::write(&dest_path, &content).expect("Failed to write htmx.min.js to destination"); + fs::write(dest_path, &content).expect("Failed to write htmx.min.js to destination"); println!("cargo:rerun-if-changed=build.rs"); } diff --git a/src/auth/session.rs b/src/auth/session.rs index f7fa004..5ec5c1c 100644 --- a/src/auth/session.rs +++ b/src/auth/session.rs @@ -24,6 +24,7 @@ pub struct Session { pub kind: SessionKind, } +#[allow(clippy::upper_case_acronyms)] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] #[sqlx(type_name = "session_kind", rename_all = "lowercase")] pub enum SessionKind { @@ -59,7 +60,7 @@ impl Sessions for User { } /// End a user session - async fn end_session(&self) -> () { + async fn end_session(&self) { sqlx::query("DELETE FROM user_session WHERE token = $1") .bind(&self.session) .execute(get_pg!()) diff --git a/src/auth/user.rs b/src/auth/user.rs index f5a5a00..8db3151 100644 --- a/src/auth/user.rs +++ b/src/auth/user.rs @@ -81,6 +81,7 @@ impl User { /// Change the password of a User /// /// Returns a Result indicating whether the password change was successful or not + #[must_use] pub async fn passwd(self, old: &str, new: &str) -> Result<(), ()> { if self.verify_pw(old) { sqlx::query("UPDATE users SET \"password\" = $1 WHERE username = $2;") @@ -97,6 +98,7 @@ impl User { } /// Find all users in the system + #[must_use] pub async fn find_all() -> Vec { sqlx::query_as("SELECT * FROM users") .fetch_all(get_pg!()) @@ -105,6 +107,7 @@ impl User { } /// Check if the user is an admin + #[must_use] pub const fn is_admin(&self) -> bool { matches!(self.user_role, UserRole::Admin) } @@ -112,6 +115,7 @@ impl User { /// Verify that a provided password matches the hashed password for the user /// /// Returns a boolean indicating whether the passwords match or not + #[must_use] pub fn verify_pw(&self, password: &str) -> bool { bcrypt::verify(password, &self.password).unwrap() } @@ -127,13 +131,12 @@ impl ToAPI for User { } /// extracts a user from a request with `session` cookie -async fn extract_user<'r>(request: &'r Request<'_>) -> Option { +async fn extract_user(request: &Request<'_>) -> Option { if let Some(session_id) = request.cookies().get("session") { if let Some(user) = User::from_session(session_id.value()).await { return Some(user); - } else { - return None; } + return None; } None @@ -146,9 +149,8 @@ impl<'r> FromRequest<'r> for User { async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { if let Some(user) = extract_user(request).await { return Outcome::Success(user); - } else { - return Outcome::Error((Status::Unauthorized, ())); } + Outcome::Error((Status::Unauthorized, ())) } } @@ -164,9 +166,8 @@ impl<'r> FromRequest<'r> for APIUser { Some(key) => { if let Some(user) = User::from_session(key).await { return Outcome::Success(APIUser(user)); - } else { - return Outcome::Error((Status::Unauthorized, ())); } + return Outcome::Error((Status::Unauthorized, ())); } None => Outcome::Error((Status::Unauthorized, ())), } @@ -202,9 +203,9 @@ impl<'r> FromRequest<'r> for MaybeUser { async fn from_request(request: &'r Request<'_>) -> rocket::request::Outcome { if let Some(user) = extract_user(request).await { return Outcome::Success(MaybeUser::User(user)); - } else { - return Outcome::Success(MaybeUser::Anonymous); } + + Outcome::Success(MaybeUser::Anonymous) } } @@ -215,13 +216,15 @@ impl From for Option { } impl MaybeUser { - pub fn user(&self) -> Option<&User> { + #[must_use] + pub const fn user(&self) -> Option<&User> { match self { MaybeUser::User(user) => Some(user), MaybeUser::Anonymous => None, } } + #[must_use] pub fn take_user(self) -> Option { match self { MaybeUser::User(user) => Some(user), @@ -255,8 +258,8 @@ impl<'r> FromRequest<'r> for AdminUser { if user.is_admin() { return Outcome::Success(AdminUser(user)); } - } else { } + Outcome::Error((Status::Unauthorized, ())) } } diff --git a/src/format.rs b/src/format.rs index 68177a6..56415e0 100644 --- a/src/format.rs +++ b/src/format.rs @@ -16,6 +16,7 @@ /// let formatted = format_date(&date); /// assert_eq!(formatted, "2023-12-18"); /// ``` +#[must_use] pub fn format_date(date: &chrono::NaiveDate) -> String { // TODO : Implement custom formatting date.to_string() @@ -37,6 +38,7 @@ pub fn format_date(date: &chrono::NaiveDate) -> String { /// let formatted = format_number(number); /// assert_eq!(formatted, "12345"); /// ``` +#[must_use] pub fn format_number(num: i32) -> String { // TODO : Implement custom formatting num.to_string() @@ -66,14 +68,15 @@ pub fn format_number(num: i32) -> String { /// let formatted = format_seconds_to_hhmmss(short_duration); /// assert_eq!(formatted, "00:59"); /// ``` +#[must_use] pub fn format_seconds_to_hhmmss(seconds: f64) -> String { let total_seconds = seconds as u64; let hours = total_seconds / 3600; let minutes = (total_seconds % 3600) / 60; let seconds = total_seconds % 60; if hours != 0 { - format!("{:02}:{:02}:{:02}", hours, minutes, seconds) + format!("{hours:02}:{minutes:02}:{seconds:02}") } else { - format!("{:02}:{:02}", minutes, seconds) + format!("{minutes:02}:{seconds:02}") } } diff --git a/src/lib.rs b/src/lib.rs index 4a72137..ffdcff6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,7 @@ pub mod result; pub static PG: OnceCell = OnceCell::const_new(); -/// A macro to retrieve or initialize the PostgreSQL connection pool. +/// A macro to retrieve or initialize the `PostgreSQL` connection pool. /// /// This macro provides a convenient way to access the `PgPool`. If the pool is not already initialized, /// it creates a new pool using the connection string from the `$DATABASE_URL` environment variable. diff --git a/src/page/mod.rs b/src/page/mod.rs index 2ada00d..45e216f 100644 --- a/src/page/mod.rs +++ b/src/page/mod.rs @@ -26,7 +26,8 @@ impl Shell { /// /// # Returns /// A `Shell` instance encapsulating the provided HTML content and attributes. - pub fn new( + #[must_use] + pub const fn new( head: PreEscaped, body_content: PreEscaped, body_class: Option, @@ -46,6 +47,7 @@ impl Shell { /// /// # Returns /// A `PreEscaped` containing the full HTML page content. + #[must_use] pub fn render(&self, content: PreEscaped, title: &str) -> PreEscaped { html! { html { @@ -94,7 +96,9 @@ pub async fn render_page( ctx: RequestContext, shell: &Shell, ) -> StringResponse { - if !ctx.is_htmx { + if ctx.is_htmx { + (Status::Ok, (ContentType::HTML, content.into_string())) + } else { ( Status::Ok, ( @@ -102,8 +106,6 @@ pub async fn render_page( shell.render(content, title).into_string(), ), ) - } else { - (Status::Ok, (ContentType::HTML, content.into_string())) } } @@ -119,6 +121,7 @@ pub async fn render_page( /// /// # Returns /// A `PreEscaped` containing the rendered HTML link element. +#[must_use] pub fn htmx_link( url: &str, class: &str, @@ -142,6 +145,7 @@ pub fn htmx_link( /// /// # Returns /// A `PreEscaped` containing the rendered `