[add] db::bundlesdb::PasteMetadata - view_password (#1)

allows pastes to require a password in order to be views
[fix] sql (mysql and psql): don't use max_lifetime
[chore] bump version (v0.8.0 -> v0.8.1)
This commit is contained in:
hkau 2024-02-23 22:47:42 -05:00
parent a5301ccfc8
commit 8417e72e7f
7 changed files with 150 additions and 14 deletions

View file

@ -3,7 +3,7 @@ name = "bundlrs"
authors = ["hkau"]
license = "MIT"
version = "0.8.0"
version = "0.8.1"
edition = "2021"
rust-version = "1.75"

View file

@ -14,7 +14,7 @@
"build:release": "./scripts/download_styles.sh && cargo build -r --no-default-features --features sqlite",
"build:release:mysql": "./scripts/download_styles.sh && cargo build -r --no-default-features --features mysql",
"build:release:postgres": "./scripts/download_styles.sh && cargo build -r --no-default-features --features postgres",
"build:docs": "cargo doc --no-deps"
"build:docs": "cargo doc --no-deps --document-private-items"
},
"dependencies": {
"@codemirror/autocomplete": "^6.12.0",

View file

@ -459,6 +459,19 @@ pub async fn get_from_url_request(
.body("You do not have permission to view this paste's contents.");
}
// if res.metadata contains a view password, return fail
if res.payload.is_some()
&& res
.clone()
.payload
.unwrap()
.metadata
.contains("\"view_password\":\"")
{
return HttpResponse::NotFound()
.body("You do not have permission to view this paste's contents.");
}
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))
@ -493,6 +506,19 @@ pub async fn get_from_id_request(
.body("You do not have permission to view this paste's contents.");
}
// if res.metadata contains a view password, return fail
if res.payload.is_some()
&& res
.clone()
.payload
.unwrap()
.metadata
.contains("\"view_password\":\"")
{
return HttpResponse::NotFound()
.body("You do not have permission to view this paste's contents.");
}
// return
return HttpResponse::Ok()
.append_header(("Content-Type", "application/json"))

View file

@ -64,6 +64,7 @@ pub struct PasteMetadata {
pub description: Option<String>,
pub favicon: Option<String>,
pub embed_color: Option<String>,
pub view_password: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -636,6 +637,7 @@ impl BundlesDB {
description: Option::Some(String::new()),
favicon: Option::None,
embed_color: Option::None,
view_password: Option::None,
})
.unwrap(),
views: 0,
@ -851,6 +853,7 @@ impl BundlesDB {
description: Option::Some(String::new()),
favicon: Option::None,
embed_color: Option::Some(String::from("#ff9999")),
view_password: Option::None,
};
// check values

View file

@ -22,8 +22,8 @@ pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::MySqlPool> {
let opts = sqlx::mysql::MySqlPoolOptions::new()
.max_connections(25)
.acquire_timeout(std::time::Duration::from_millis(1000))
.idle_timeout(Some(std::time::Duration::from_secs(60)))
.max_lifetime(Some(std::time::Duration::from_secs(120)));
.idle_timeout(Some(std::time::Duration::from_secs(60)));
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
let client = opts
.connect(&format!(
@ -56,8 +56,8 @@ pub async fn create_db(options: DatabaseOpts) -> Database<sqlx::PgPool> {
let opts = sqlx::postgres::PgPoolOptions::new()
.max_connections(25)
.acquire_timeout(std::time::Duration::from_millis(1000))
.idle_timeout(Some(std::time::Duration::from_secs(60)))
.max_lifetime(Some(std::time::Duration::from_secs(120)));
.idle_timeout(Some(std::time::Duration::from_secs(60)));
// .max_lifetime(Some(std::time::Duration::from_secs(120)));
let client = opts
.connect(&format!(

View file

@ -11,16 +11,22 @@ use crate::utility::format_html;
use crate::components::navigation::Footer;
#[derive(Default, Properties, PartialEq)]
struct Props {
pub struct Props {
pub paste: Paste<String>,
pub auth_state: Option<bool>,
}
#[derive(Default, Properties, PartialEq, serde::Deserialize)]
pub struct PasteViewProps {
pub view: Option<String>,
}
#[function_component]
fn PasteView(props: &Props) -> Html {
let content = Html::from_html_unchecked(AttrValue::from(props.paste.content_html.clone()));
let metadata = serde_json::from_str::<bundlesdb::PasteMetadata>(&props.paste.metadata).unwrap();
// default return
return html! {
<main class="flex flex-column g-4">
<div id="secret" />
@ -33,7 +39,7 @@ fn PasteView(props: &Props) -> Html {
{content}
</div>
<div class="flex justify-space-between g-4 full">
<div class="flex justify-space-between g-4 full" id="paste-info-box">
<div class="flex g-4 flex-wrap mobile:flex-column">
<a class="button round" href={format!("/?editing={}", &props.paste.custom_url)}>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-pencil"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
@ -78,9 +84,53 @@ fn build_renderer_with_props(props: Props) -> ServerRenderer<PasteView> {
return ServerRenderer::<PasteView>::with_props(|| props);
}
#[function_component]
pub fn PastePasswordAsk(props: &Props) -> Html {
// default return
return html! {
<div class="flex flex-column g-4" style="height: 100dvh;">
<main class="small flex flex-column g-4 align-center">
<div class="card secondary round border" style="width: 25rem;" id="forms">
<h2 class="no-margin text-center full">{props.paste.custom_url.clone()}</h2>
<hr />
<form class="full flex flex-column g-4" id="login-to-paste">
<label for="view"><b>{"View Password"}</b></label>
<input
type="text"
name="view"
id="view"
placeholder="Paste View Password"
class="full round"
minlength={4}
maxlength={32}
/>
<hr />
<button class="bundles-primary full round">
{"Continue"}
</button>
</form>
</div>
</main>
</div>
};
}
pub fn build_password_ask_renderer_with_props(props: Props) -> ServerRenderer<PastePasswordAsk> {
return ServerRenderer::<PastePasswordAsk>::with_props(|| props);
}
#[get("/{url:.*}")]
/// Available at "/{custom_url}"
pub async fn paste_view_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
pub async fn paste_view_request(
req: HttpRequest,
data: web::Data<AppData>,
info: web::Query<PasteViewProps>,
) -> impl Responder {
// get paste
let url: String = req.match_info().get("url").unwrap().to_string();
let url_c = url.clone();
@ -100,6 +150,33 @@ pub async fn paste_view_request(req: HttpRequest, data: web::Data<AppData>) -> i
let unwrap = paste.payload.as_ref().unwrap();
// ...
let metadata = serde_json::from_str::<bundlesdb::PasteMetadata>(&unwrap.metadata).unwrap();
// handle view password
if metadata.view_password.is_some() && info.view.is_none() {
let renderer = build_password_ask_renderer_with_props(Props {
paste: unwrap.clone(),
auth_state: if req.cookie("__Secure-Token").is_some() {
Option::Some(req.cookie("__Secure-Token").is_some())
} else {
Option::Some(false)
},
});
let render = renderer.render();
return HttpResponse::Ok()
.append_header(("Set-Cookie", ""))
.append_header(("Content-Type", "text/html"))
.body(format_html(render.await, ""));
}
// (check password)
if info.view.is_some() && info.view.as_ref().unwrap() != &metadata.view_password.unwrap() {
return HttpResponse::NotFound()
.body("You do not have permission to view this paste's contents.");
}
// handle atomic pastes (just return index.html)
if unwrap.content.contains("\"_is_atomic\":true") {
let real_content = serde_json::from_str::<bundlesdb::AtomicPaste>(&unwrap.content);
@ -121,9 +198,6 @@ pub async fn paste_view_request(req: HttpRequest, data: web::Data<AppData>) -> i
.body(index_html.unwrap().content.clone());
}
// ...
let metadata = serde_json::from_str::<bundlesdb::PasteMetadata>(&unwrap.metadata).unwrap();
// verify auth status
let token_cookie = req.cookie("__Secure-Token");
let mut set_cookie: &str = "";

View file

@ -8,6 +8,7 @@ use crate::db::bundlesdb::{self, AppData, Paste};
use crate::utility::format_html;
use crate::components::navigation::Footer;
use crate::pages::paste_view;
#[derive(Default, Properties, PartialEq)]
struct Props {
@ -131,7 +132,11 @@ pub async fn user_settings_request(req: HttpRequest, data: web::Data<AppData>) -
#[get("/d/settings/paste/{url:.*}")]
/// Available at "/d/settings/paste/{custom_url}"
pub async fn paste_settings_request(req: HttpRequest, data: web::Data<AppData>) -> impl Responder {
pub async fn paste_settings_request(
req: HttpRequest,
data: web::Data<AppData>,
info: web::Query<paste_view::PasteViewProps>,
) -> impl Responder {
// get paste
let url: String = req.match_info().get("url").unwrap().to_string();
let url_c = url.clone();
@ -164,9 +169,37 @@ pub async fn paste_settings_request(req: HttpRequest, data: web::Data<AppData>)
}
}
// ...
let unwrap = paste.payload.clone().unwrap();
let metadata = serde_json::from_str::<bundlesdb::PasteMetadata>(&unwrap.metadata).unwrap();
// handle view password
if metadata.view_password.is_some() && info.view.is_none() {
let renderer = paste_view::build_password_ask_renderer_with_props(paste_view::Props {
paste: unwrap,
auth_state: if req.cookie("__Secure-Token").is_some() {
Option::Some(req.cookie("__Secure-Token").is_some())
} else {
Option::Some(false)
},
});
let render = renderer.render();
return HttpResponse::Ok()
.append_header(("Set-Cookie", ""))
.append_header(("Content-Type", "text/html"))
.body(format_html(render.await, ""));
}
// (check password)
if info.view.is_some() && info.view.as_ref().unwrap() != &metadata.view_password.unwrap() {
return HttpResponse::NotFound()
.body("You do not have permission to view this paste's contents.");
}
// ...
let renderer = build_paste_settings_with_props(Props {
paste: paste.payload.unwrap(),
paste: paste.payload.clone().unwrap(),
auth_state: if req.cookie("__Secure-Token").is_some() {
Option::Some(req.cookie("__Secure-Token").is_some())
} else {