-
Notifications
You must be signed in to change notification settings - Fork 32
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
577 additions
and
530 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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())) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }) | ||
} |
Oops, something went wrong.