Skip to content

Commit 40523ec

Browse files
committed
Restructure routes, handlers foo
1 parent 79d7c3d commit 40523ec

File tree

23 files changed

+1133
-1047
lines changed

23 files changed

+1133
-1047
lines changed

src/db.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,23 @@ impl Database {
346346
Ok(uid)
347347
}
348348

349+
/// Get title of a paste.
350+
pub async fn get_title(&self, id: Id) -> Result<String, Error> {
351+
let conn = self.conn.clone();
352+
let id = id.as_u32();
353+
354+
let title = spawn_blocking(move || {
355+
conn.lock().query_row(
356+
"SELECT title FROM entries WHERE id=?1",
357+
params![id],
358+
|row| Ok(row.get(0)?),
359+
)
360+
})
361+
.await??;
362+
363+
Ok(title)
364+
}
365+
349366
/// Delete `id`.
350367
pub async fn delete(&self, id: Id) -> Result<(), Error> {
351368
let conn = self.conn.clone();

src/handlers/delete.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use crate::handlers::html::{make_error, ErrorResponse};
2+
use crate::{Database, Error, Page};
3+
use axum::extract::{Path, State};
4+
use axum::response::Redirect;
5+
use axum_extra::extract::SignedCookieJar;
6+
7+
pub async fn delete(
8+
Path(id): Path<String>,
9+
State(db): State<Database>,
10+
State(page): State<Page>,
11+
jar: SignedCookieJar,
12+
) -> Result<Redirect, ErrorResponse> {
13+
async {
14+
let id = id.parse()?;
15+
let uid = db.get_uid(id).await?;
16+
let can_delete = jar
17+
.get("uid")
18+
.map(|cookie| cookie.value().parse::<i64>())
19+
.transpose()
20+
.map_err(|err| Error::CookieParsing(err.to_string()))?
21+
.zip(uid)
22+
.is_some_and(|(user_uid, db_uid)| user_uid == db_uid);
23+
24+
if !can_delete {
25+
Err(Error::Delete)?;
26+
}
27+
28+
db.delete(id).await?;
29+
30+
Ok(Redirect::to("/"))
31+
}
32+
.await
33+
.map_err(|err| make_error(err, page.clone()))
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use crate::test_helpers::Client;
39+
use reqwest::StatusCode;
40+
41+
#[tokio::test]
42+
async fn delete_via_link() -> Result<(), Box<dyn std::error::Error>> {
43+
let client = Client::new().await;
44+
45+
let data = crate::handlers::insert::form::Entry {
46+
text: "FooBarBaz".to_string(),
47+
extension: None,
48+
expires: "0".to_string(),
49+
password: "".to_string(),
50+
title: "".to_string(),
51+
};
52+
53+
let res = client.post("/").form(&data).send().await?;
54+
let uid_cookie = res.cookies().find(|cookie| cookie.name() == "uid").unwrap();
55+
assert_eq!(uid_cookie.name(), "uid");
56+
assert!(uid_cookie.value().len() > 40);
57+
assert_eq!(uid_cookie.path(), None);
58+
assert!(uid_cookie.http_only());
59+
assert!(uid_cookie.same_site_strict());
60+
assert!(!uid_cookie.secure());
61+
assert_eq!(uid_cookie.domain(), None);
62+
assert_eq!(uid_cookie.expires(), None);
63+
assert_eq!(uid_cookie.max_age(), None);
64+
65+
assert_eq!(res.status(), StatusCode::SEE_OTHER);
66+
67+
let location = res.headers().get("location").unwrap().to_str()?;
68+
let id = location.replace("/", "");
69+
70+
let res = client.get(&format!("/delete/{id}")).send().await?;
71+
assert_eq!(res.status(), StatusCode::SEE_OTHER);
72+
73+
let res = client.get(&format!("/{id}")).send().await?;
74+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
75+
76+
Ok(())
77+
}
78+
}

src/handlers/download.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use crate::cache::Key;
2+
use crate::crypto::Password;
3+
use crate::handlers::html::{make_error, ErrorResponse, PasswordInput};
4+
use crate::{Database, Error, Page};
5+
use axum::extract::{Form, Path, State};
6+
use axum::http::header;
7+
use axum::response::{AppendHeaders, IntoResponse, Response};
8+
use axum_extra::headers::HeaderValue;
9+
use serde::Deserialize;
10+
11+
#[derive(Deserialize, Debug)]
12+
pub struct PasswordForm {
13+
password: String,
14+
}
15+
16+
/// GET handler for raw content of a paste.
17+
pub async fn download(
18+
Path(id): Path<String>,
19+
State(db): State<Database>,
20+
State(page): State<Page>,
21+
form: Option<Form<PasswordForm>>,
22+
) -> Result<Response, ErrorResponse> {
23+
async {
24+
let password = form.map(|form| Password::from(form.password.as_bytes().to_vec()));
25+
let key: Key = id.parse()?;
26+
27+
match db.get(key.id, password.clone()).await {
28+
Err(Error::NoPassword) => Ok(PasswordInput {
29+
page: page.clone(),
30+
id: key.id.to_string(),
31+
}
32+
.into_response()),
33+
Err(err) => Err(err),
34+
Ok(entry) => {
35+
if entry.must_be_deleted {
36+
db.delete(key.id).await?;
37+
}
38+
39+
Ok(get_download(entry.text, &key.id(), &key.ext).into_response())
40+
}
41+
}
42+
}
43+
.await
44+
.map_err(|err| make_error(err, page))
45+
}
46+
47+
fn get_download(text: String, id: &str, extension: &str) -> impl IntoResponse {
48+
let content_type = "text; charset=utf-8";
49+
let content_disposition =
50+
HeaderValue::from_str(&format!(r#"attachment; filename="{id}.{extension}"#))
51+
.expect("constructing valid header value");
52+
53+
(
54+
AppendHeaders([
55+
(header::CONTENT_TYPE, HeaderValue::from_static(content_type)),
56+
(header::CONTENT_DISPOSITION, content_disposition),
57+
]),
58+
text,
59+
)
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use crate::test_helpers::Client;
65+
use reqwest::StatusCode;
66+
67+
#[tokio::test]
68+
async fn download() -> Result<(), Box<dyn std::error::Error>> {
69+
let client = Client::new().await;
70+
71+
let data = crate::handlers::insert::form::Entry {
72+
text: "FooBarBaz".to_string(),
73+
extension: None,
74+
expires: "0".to_string(),
75+
password: "".to_string(),
76+
title: "".to_string(),
77+
};
78+
79+
let res = client.post("/").form(&data).send().await?;
80+
assert_eq!(res.status(), StatusCode::SEE_OTHER);
81+
82+
let location = res.headers().get("location").unwrap().to_str()?;
83+
let res = client.get(&format!("{location}?dl=cpp")).send().await?;
84+
assert_eq!(res.status(), StatusCode::OK);
85+
86+
let content = res.text().await?;
87+
assert_eq!(content, "FooBarBaz");
88+
89+
Ok(())
90+
}
91+
}

src/handlers/html/burn.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use crate::handlers::html::qr::{code_from, dark_modules};
2+
use crate::handlers::html::{make_error, ErrorResponse};
3+
use crate::{Error, Page};
4+
use askama::Template;
5+
use axum::extract::{Path, State};
6+
7+
/// GET handler for the burn page.
8+
pub async fn burn(Path(id): Path<String>, State(page): State<Page>) -> Result<Burn, ErrorResponse> {
9+
async {
10+
let code = tokio::task::spawn_blocking({
11+
let page = page.clone();
12+
let id = id.clone();
13+
move || code_from(&page.base_url, id)
14+
})
15+
.await
16+
.map_err(Error::from)??;
17+
18+
Ok(Burn {
19+
page: page.clone(),
20+
id,
21+
code,
22+
})
23+
}
24+
.await
25+
.map_err(|err| make_error(err, page))
26+
}
27+
28+
/// Burn page shown if "burn-after-reading" was selected during insertion.
29+
#[derive(Template)]
30+
#[template(path = "burn.html", escape = "none")]
31+
pub struct Burn {
32+
page: Page,
33+
id: String,
34+
code: qrcodegen::QrCode,
35+
}
36+
37+
impl Burn {
38+
fn dark_modules(&self) -> Vec<(i32, i32)> {
39+
dark_modules(&self.code)
40+
}
41+
}
42+
43+
#[cfg(test)]
44+
mod tests {
45+
use crate::test_helpers::Client;
46+
use reqwest::{header, StatusCode};
47+
use serde::Serialize;
48+
49+
#[tokio::test]
50+
async fn burn() -> Result<(), Box<dyn std::error::Error>> {
51+
let client = Client::new().await;
52+
53+
let data = crate::handlers::insert::form::Entry {
54+
text: "FooBarBaz".to_string(),
55+
extension: None,
56+
expires: "burn".to_string(),
57+
password: "".to_string(),
58+
title: "".to_string(),
59+
};
60+
61+
let res = client.post("/").form(&data).send().await?;
62+
assert_eq!(res.status(), StatusCode::SEE_OTHER);
63+
64+
let location = res.headers().get("location").unwrap().to_str()?;
65+
66+
// Location is the `/burn/foo` page not the paste itself, so remove the prefix.
67+
let location = location.replace("burn/", "");
68+
69+
let res = client
70+
.get(&location)
71+
.header(header::ACCEPT, "text/html; charset=utf-8")
72+
.send()
73+
.await?;
74+
75+
assert_eq!(res.status(), StatusCode::OK);
76+
77+
let res = client
78+
.get(&location)
79+
.header(header::ACCEPT, "text/html; charset=utf-8")
80+
.send()
81+
.await?;
82+
83+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
84+
85+
Ok(())
86+
}
87+
88+
#[tokio::test]
89+
async fn burn_encrypted() -> Result<(), Box<dyn std::error::Error>> {
90+
let client = Client::new().await;
91+
let password = "asd";
92+
93+
let data = crate::handlers::insert::form::Entry {
94+
text: "FooBarBaz".to_string(),
95+
extension: None,
96+
expires: "burn".to_string(),
97+
password: password.to_string(),
98+
title: "".to_string(),
99+
};
100+
101+
let res = client.post("/").form(&data).send().await?;
102+
assert_eq!(res.status(), StatusCode::SEE_OTHER);
103+
104+
let location = res.headers().get("location").unwrap().to_str()?;
105+
106+
// Location is the `/burn/foo` page not the paste itself, so remove the prefix.
107+
let location = location.replace("burn/", "");
108+
109+
let res = client
110+
.get(&location)
111+
.header(header::ACCEPT, "text/html; charset=utf-8")
112+
.send()
113+
.await?;
114+
115+
assert_eq!(res.status(), StatusCode::OK);
116+
117+
#[derive(Debug, Serialize)]
118+
struct Form {
119+
password: String,
120+
}
121+
122+
let data = Form {
123+
password: password.to_string(),
124+
};
125+
126+
let res = client
127+
.post(&location)
128+
.form(&data)
129+
.header(header::ACCEPT, "text/html; charset=utf-8")
130+
.send()
131+
.await?;
132+
133+
assert_eq!(res.status(), StatusCode::OK);
134+
135+
let res = client
136+
.get(&location)
137+
.header(header::ACCEPT, "text/html; charset=utf-8")
138+
.send()
139+
.await?;
140+
141+
assert_eq!(res.status(), StatusCode::NOT_FOUND);
142+
143+
Ok(())
144+
}
145+
}

0 commit comments

Comments
 (0)