This commit is contained in:
parent
3837302161
commit
439467f730
10 changed files with 45 additions and 32 deletions
4
build.rs
4
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");
|
||||
}
|
||||
|
|
|
@ -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!())
|
||||
|
|
|
@ -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<Self> {
|
||||
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<User> {
|
||||
async fn extract_user(request: &Request<'_>) -> Option<User> {
|
||||
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<Self, Self::Error> {
|
||||
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<Self, Self::Error> {
|
||||
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<MaybeUser> for Option<User> {
|
|||
}
|
||||
|
||||
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<User> {
|
||||
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, ()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub mod result;
|
|||
|
||||
pub static PG: OnceCell<sqlx::PgPool> = 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.
|
||||
|
|
|
@ -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<String>,
|
||||
body_content: PreEscaped<String>,
|
||||
body_class: Option<String>,
|
||||
|
@ -46,6 +47,7 @@ impl Shell {
|
|||
///
|
||||
/// # Returns
|
||||
/// A `PreEscaped<String>` containing the full HTML page content.
|
||||
#[must_use]
|
||||
pub fn render(&self, content: PreEscaped<String>, title: &str) -> PreEscaped<String> {
|
||||
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<String>` 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<String>` containing the rendered `<script>` element.
|
||||
#[must_use]
|
||||
pub fn script(script: &str) -> PreEscaped<String> {
|
||||
html!(
|
||||
script {
|
||||
|
|
|
@ -62,6 +62,7 @@ pub fn to_uuid(id: &str) -> Result<uuid::Uuid, ApiError> {
|
|||
///
|
||||
/// # Returns
|
||||
/// * `ApiError` - A `BadRequest` error with a JSON payload describing the issue.
|
||||
#[must_use]
|
||||
pub fn no_uuid_error() -> ApiError {
|
||||
api_error("No valid UUID")
|
||||
}
|
||||
|
@ -76,6 +77,7 @@ pub fn no_uuid_error() -> ApiError {
|
|||
///
|
||||
/// # Returns
|
||||
/// * `ApiError` - A `BadRequest` error with a JSON payload describing the issue.
|
||||
#[must_use]
|
||||
pub fn api_error(msg: &str) -> ApiError {
|
||||
BadRequest(json!({
|
||||
"error": msg
|
||||
|
|
|
@ -14,6 +14,7 @@ pub struct DataResponse {
|
|||
}
|
||||
|
||||
impl DataResponse {
|
||||
#[must_use]
|
||||
pub fn new(data: Vec<u8>, content_type: &str, cache_duration: Option<u64>) -> Self {
|
||||
Self {
|
||||
data,
|
||||
|
@ -47,11 +48,10 @@ impl<'r> Responder<'r, 'static> for DataResponse {
|
|||
}
|
||||
|
||||
// Add caching headers for static files
|
||||
let cache_control_header = if let Some(duration) = self.cache_duration {
|
||||
Header::new("Cache-Control", format!("public, max-age={}", duration))
|
||||
} else {
|
||||
Header::new("Cache-Control", "no-cache")
|
||||
};
|
||||
let cache_control_header = self.cache_duration.map_or_else(
|
||||
|| Header::new("Cache-Control", "no-cache"),
|
||||
|duration| Header::new("Cache-Control", format!("public, max-age={duration}")),
|
||||
);
|
||||
|
||||
Ok(Response::build()
|
||||
.header(cache_control_header)
|
||||
|
|
|
@ -17,11 +17,7 @@ impl<'r> FromRequest<'r> for RequestContext {
|
|||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
rocket::outcome::Outcome::Success(RequestContext {
|
||||
is_htmx: !req
|
||||
.headers()
|
||||
.get("HX-Request")
|
||||
.collect::<Vec<&str>>()
|
||||
.is_empty(),
|
||||
is_htmx: req.headers().get("HX-Request").next().is_some(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ impl RespondRaw for StringResponse {
|
|||
///
|
||||
/// # Returns
|
||||
/// A `StringResponse` with status `200 OK`, content type `application/json`, and the JSON-encoded body.
|
||||
#[must_use]
|
||||
pub fn respond_json(json: &serde_json::Value) -> StringResponse {
|
||||
(
|
||||
Status::Ok,
|
||||
|
@ -65,6 +66,7 @@ pub fn respond_json(json: &serde_json::Value) -> StringResponse {
|
|||
///
|
||||
/// # Returns
|
||||
/// A `StringResponse` with status `200 OK`, content type `text/html`, and the HTML content as the body.
|
||||
#[must_use]
|
||||
pub fn respond_html(html: &str) -> StringResponse {
|
||||
(Status::Ok, (ContentType::HTML, html.to_string()))
|
||||
}
|
||||
|
@ -76,6 +78,7 @@ pub fn respond_html(html: &str) -> StringResponse {
|
|||
///
|
||||
/// # Returns
|
||||
/// A `StringResponse` with status `200 OK`, content type `text/javascript`, and the JS content as the body.
|
||||
#[must_use]
|
||||
pub fn respond_script(script: &str) -> StringResponse {
|
||||
(Status::Ok, (ContentType::JavaScript, script.to_string()))
|
||||
}
|
||||
|
@ -89,6 +92,7 @@ pub fn respond_script(script: &str) -> StringResponse {
|
|||
///
|
||||
/// # Returns
|
||||
/// A `RawResponse` containing the provided status, content type, and body.
|
||||
pub fn respond_with(status: Status, content_type: ContentType, body: Vec<u8>) -> RawResponse {
|
||||
#[must_use]
|
||||
pub const fn respond_with(status: Status, content_type: ContentType, body: Vec<u8>) -> RawResponse {
|
||||
(status, (content_type, body))
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue