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 8, 2025
1 parent 79d7c3d commit e3f73f2
Show file tree
Hide file tree
Showing 19 changed files with 577 additions and 530 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
34 changes: 34 additions & 0 deletions src/handlers/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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()))
}
60 changes: 60 additions & 0 deletions src/handlers/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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,
)
}
41 changes: 41 additions & 0 deletions src/handlers/html/burn.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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)
}
}
96 changes: 96 additions & 0 deletions src/handlers/html/index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use crate::{AppState, Highlighter, Page};
use askama::Template;
use axum::extract::State;
use std::num::{NonZero, NonZeroU32};
use std::sync::OnceLock;

/// GET handler for the index page.
pub async fn index(
State(state): State<AppState>,
State(page): State<Page>,
State(highlighter): State<Highlighter>,
) -> Index {
Index {
page,
max_expiration: state.max_expiration,
highlighter,
}
}

/// Index page displaying a form for paste insertion and a selection box for languages.
#[derive(Template)]
#[template(path = "index.html")]
pub struct Index {
page: Page,
max_expiration: Option<NonZeroU32>,
highlighter: Highlighter,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum Expiration {
None,
Burn,
Time(NonZeroU32),
}

impl std::fmt::Display for Expiration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expiration::None => write!(f, ""),
Expiration::Burn => write!(f, "burn"),
Expiration::Time(t) => write!(f, "{t}"),
}
}
}

#[allow(clippy::unwrap_used)]
const EXPIRATION_OPTIONS: [(&str, Expiration); 8] = [
("never", Expiration::None),
("10 minutes", Expiration::Time(NonZero::new(600).unwrap())),
("1 hour", Expiration::Time(NonZero::new(3600).unwrap())),
("1 day", Expiration::Time(NonZero::new(86400).unwrap())),
("1 week", Expiration::Time(NonZero::new(604_800).unwrap())),
(
"1 month",
Expiration::Time(NonZero::new(2_592_000).unwrap()),
),
(
"1 year",
Expiration::Time(NonZero::new(31_536_000).unwrap()),
),
("🔥 after reading", Expiration::Burn),
];

impl Index {
fn expiry_options(&self) -> &str {
static EXPIRATION_OPTIONS_HTML: OnceLock<String> = OnceLock::new();

EXPIRATION_OPTIONS_HTML.get_or_init(|| {

let mut option_set = String::new();
let mut wrote_first = false;

option_set.push('\n');

for (opt_name, opt_val) in EXPIRATION_OPTIONS {
if self.max_expiration.is_none()
|| opt_val == Expiration::Burn
|| matches!((self.max_expiration, opt_val), (Some(exp), Expiration::Time(time)) if time <= exp)
{
option_set.push_str("<option");
if !wrote_first {
option_set.push_str(" selected");
wrote_first = true;
}
option_set.push_str(" value=\"");
option_set.push_str(opt_val.to_string().as_ref());
option_set.push_str("\">");
option_set.push_str(opt_name);
option_set.push_str("</option>\n");
}
}

option_set
})
}
}
38 changes: 38 additions & 0 deletions src/handlers/html/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
pub mod burn;
pub mod index;
pub mod paste;
pub mod qr;

pub use burn::burn;
pub use index::index;
pub use qr::qr;

use crate::{errors, Page};
use askama::Template;
use axum::http::StatusCode;

/// Error page showing a message.
#[derive(Template)]
#[template(path = "error.html")]
pub struct Error {
pub page: Page,
pub description: String,
}

/// Page showing password input.
#[derive(Template)]
#[template(path = "encrypted.html")]
pub struct PasswordInput {
pub page: Page,
pub id: String,
}

/// Error response carrying a status code and the page itself.
pub type ErrorResponse = (StatusCode, Error);

/// Create an error response from `error` consisting of [`StatusCode`] derive from `error` as well
/// as a rendered page with a description.
pub fn make_error(error: errors::Error, page: Page) -> ErrorResponse {
let description = error.to_string();
(error.into(), Error { page, description })
}
Loading

0 comments on commit e3f73f2

Please # to comment.