Skip to content

Commit

Permalink
Restructure routes, handlers foo
Browse files Browse the repository at this point in the history
  • Loading branch information
matze committed Feb 11, 2025
1 parent 79d7c3d commit 40523ec
Show file tree
Hide file tree
Showing 23 changed files with 1,133 additions and 1,047 deletions.
17 changes: 17 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,23 @@ impl Database {
Ok(uid)
}

/// Get title of a paste.
pub async fn get_title(&self, id: Id) -> Result<String, Error> {
let conn = self.conn.clone();
let id = id.as_u32();

let title = spawn_blocking(move || {
conn.lock().query_row(
"SELECT title FROM entries WHERE id=?1",
params![id],
|row| Ok(row.get(0)?),
)
})
.await??;

Ok(title)
}

/// Delete `id`.
pub async fn delete(&self, id: Id) -> Result<(), Error> {
let conn = self.conn.clone();
Expand Down
78 changes: 78 additions & 0 deletions src/handlers/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::handlers::html::{make_error, ErrorResponse};
use crate::{Database, Error, Page};
use axum::extract::{Path, State};
use axum::response::Redirect;
use axum_extra::extract::SignedCookieJar;

pub async fn delete(
Path(id): Path<String>,
State(db): State<Database>,
State(page): State<Page>,
jar: SignedCookieJar,
) -> Result<Redirect, ErrorResponse> {
async {
let id = id.parse()?;
let uid = db.get_uid(id).await?;
let can_delete = jar
.get("uid")
.map(|cookie| cookie.value().parse::<i64>())
.transpose()
.map_err(|err| Error::CookieParsing(err.to_string()))?
.zip(uid)
.is_some_and(|(user_uid, db_uid)| user_uid == db_uid);

if !can_delete {
Err(Error::Delete)?;
}

db.delete(id).await?;

Ok(Redirect::to("/"))
}
.await
.map_err(|err| make_error(err, page.clone()))
}

#[cfg(test)]
mod tests {
use crate::test_helpers::Client;
use reqwest::StatusCode;

#[tokio::test]
async fn delete_via_link() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new().await;

let data = crate::handlers::insert::form::Entry {
text: "FooBarBaz".to_string(),
extension: None,
expires: "0".to_string(),
password: "".to_string(),
title: "".to_string(),
};

let res = client.post("/").form(&data).send().await?;
let uid_cookie = res.cookies().find(|cookie| cookie.name() == "uid").unwrap();
assert_eq!(uid_cookie.name(), "uid");
assert!(uid_cookie.value().len() > 40);
assert_eq!(uid_cookie.path(), None);
assert!(uid_cookie.http_only());
assert!(uid_cookie.same_site_strict());
assert!(!uid_cookie.secure());
assert_eq!(uid_cookie.domain(), None);
assert_eq!(uid_cookie.expires(), None);
assert_eq!(uid_cookie.max_age(), None);

assert_eq!(res.status(), StatusCode::SEE_OTHER);

let location = res.headers().get("location").unwrap().to_str()?;
let id = location.replace("/", "");

let res = client.get(&format!("/delete/{id}")).send().await?;
assert_eq!(res.status(), StatusCode::SEE_OTHER);

let res = client.get(&format!("/{id}")).send().await?;
assert_eq!(res.status(), StatusCode::NOT_FOUND);

Ok(())
}
}
91 changes: 91 additions & 0 deletions src/handlers/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use crate::cache::Key;
use crate::crypto::Password;
use crate::handlers::html::{make_error, ErrorResponse, PasswordInput};
use crate::{Database, Error, Page};
use axum::extract::{Form, Path, State};
use axum::http::header;
use axum::response::{AppendHeaders, IntoResponse, Response};
use axum_extra::headers::HeaderValue;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct PasswordForm {
password: String,
}

/// GET handler for raw content of a paste.
pub async fn download(
Path(id): Path<String>,
State(db): State<Database>,
State(page): State<Page>,
form: Option<Form<PasswordForm>>,
) -> Result<Response, ErrorResponse> {
async {
let password = form.map(|form| Password::from(form.password.as_bytes().to_vec()));
let key: Key = id.parse()?;

match db.get(key.id, password.clone()).await {
Err(Error::NoPassword) => Ok(PasswordInput {
page: page.clone(),
id: key.id.to_string(),
}
.into_response()),
Err(err) => Err(err),
Ok(entry) => {
if entry.must_be_deleted {
db.delete(key.id).await?;
}

Ok(get_download(entry.text, &key.id(), &key.ext).into_response())
}
}
}
.await
.map_err(|err| make_error(err, page))
}

fn get_download(text: String, id: &str, extension: &str) -> impl IntoResponse {
let content_type = "text; charset=utf-8";
let content_disposition =
HeaderValue::from_str(&format!(r#"attachment; filename="{id}.{extension}"#))
.expect("constructing valid header value");

(
AppendHeaders([
(header::CONTENT_TYPE, HeaderValue::from_static(content_type)),
(header::CONTENT_DISPOSITION, content_disposition),
]),
text,
)
}

#[cfg(test)]
mod tests {
use crate::test_helpers::Client;
use reqwest::StatusCode;

#[tokio::test]
async fn download() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new().await;

let data = crate::handlers::insert::form::Entry {
text: "FooBarBaz".to_string(),
extension: None,
expires: "0".to_string(),
password: "".to_string(),
title: "".to_string(),
};

let res = client.post("/").form(&data).send().await?;
assert_eq!(res.status(), StatusCode::SEE_OTHER);

let location = res.headers().get("location").unwrap().to_str()?;
let res = client.get(&format!("{location}?dl=cpp")).send().await?;
assert_eq!(res.status(), StatusCode::OK);

let content = res.text().await?;
assert_eq!(content, "FooBarBaz");

Ok(())
}
}
145 changes: 145 additions & 0 deletions src/handlers/html/burn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::handlers::html::qr::{code_from, dark_modules};
use crate::handlers::html::{make_error, ErrorResponse};
use crate::{Error, Page};
use askama::Template;
use axum::extract::{Path, State};

/// GET handler for the burn page.
pub async fn burn(Path(id): Path<String>, State(page): State<Page>) -> Result<Burn, ErrorResponse> {
async {
let code = tokio::task::spawn_blocking({
let page = page.clone();
let id = id.clone();
move || code_from(&page.base_url, id)
})
.await
.map_err(Error::from)??;

Ok(Burn {
page: page.clone(),
id,
code,
})
}
.await
.map_err(|err| make_error(err, page))
}

/// Burn page shown if "burn-after-reading" was selected during insertion.
#[derive(Template)]
#[template(path = "burn.html", escape = "none")]
pub struct Burn {
page: Page,
id: String,
code: qrcodegen::QrCode,
}

impl Burn {
fn dark_modules(&self) -> Vec<(i32, i32)> {
dark_modules(&self.code)
}
}

#[cfg(test)]
mod tests {
use crate::test_helpers::Client;
use reqwest::{header, StatusCode};
use serde::Serialize;

#[tokio::test]
async fn burn() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new().await;

let data = crate::handlers::insert::form::Entry {
text: "FooBarBaz".to_string(),
extension: None,
expires: "burn".to_string(),
password: "".to_string(),
title: "".to_string(),
};

let res = client.post("/").form(&data).send().await?;
assert_eq!(res.status(), StatusCode::SEE_OTHER);

let location = res.headers().get("location").unwrap().to_str()?;

// Location is the `/burn/foo` page not the paste itself, so remove the prefix.
let location = location.replace("burn/", "");

let res = client
.get(&location)
.header(header::ACCEPT, "text/html; charset=utf-8")
.send()
.await?;

assert_eq!(res.status(), StatusCode::OK);

let res = client
.get(&location)
.header(header::ACCEPT, "text/html; charset=utf-8")
.send()
.await?;

assert_eq!(res.status(), StatusCode::NOT_FOUND);

Ok(())
}

#[tokio::test]
async fn burn_encrypted() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new().await;
let password = "asd";

let data = crate::handlers::insert::form::Entry {
text: "FooBarBaz".to_string(),
extension: None,
expires: "burn".to_string(),
password: password.to_string(),
title: "".to_string(),
};

let res = client.post("/").form(&data).send().await?;
assert_eq!(res.status(), StatusCode::SEE_OTHER);

let location = res.headers().get("location").unwrap().to_str()?;

// Location is the `/burn/foo` page not the paste itself, so remove the prefix.
let location = location.replace("burn/", "");

let res = client
.get(&location)
.header(header::ACCEPT, "text/html; charset=utf-8")
.send()
.await?;

assert_eq!(res.status(), StatusCode::OK);

#[derive(Debug, Serialize)]
struct Form {
password: String,
}

let data = Form {
password: password.to_string(),
};

let res = client
.post(&location)
.form(&data)
.header(header::ACCEPT, "text/html; charset=utf-8")
.send()
.await?;

assert_eq!(res.status(), StatusCode::OK);

let res = client
.get(&location)
.header(header::ACCEPT, "text/html; charset=utf-8")
.send()
.await?;

assert_eq!(res.status(), StatusCode::NOT_FOUND);

Ok(())
}
}
Loading

0 comments on commit 40523ec

Please sign in to comment.