add: option_source_password and option_view_password

This commit is contained in:
trisua 2025-07-25 20:26:22 -04:00
parent a49efdd238
commit 2de840f50d
5 changed files with 158 additions and 13 deletions

View file

@ -180,6 +180,11 @@ video {
color: var(--color-text);
}
.card-nest .card:nth-child(1) {
background: var(--color-super-raised);
padding: var(--pad-2) var(--pad-4);
}
/* button */
.button {
--h: 36px;
@ -307,6 +312,10 @@ input[data-invalid] {
border-left: inset 5px var(--color-red);
}
input.surface {
background: var(--color-surface);
}
/* typo */
p {
margin-bottom: var(--pad-4);

View file

@ -0,0 +1,32 @@
(text "{% extends \"root.lisp\" %} {% block head %}")
(title
(text "{{ entry.slug }}"))
(link ("rel" "icon") ("href" "/public/favicon.svg"))
(text "{% endblock %} {% block body %}")
(main
("class" "card-nest")
(div
("class" "card flex items-center gap-2 no_fill")
(text "{{ icon \"lock\" }}")
(b (text "Password required")))
(form
("class" "card flex flex-col gap-2")
("onsubmit" "use_password(event)")
(div
("class" "flex flex-collapse gap-2")
(input
("class" "surface")
("required" "")
("placeholder" "Password")
("name" "password"))
(button
("class" "button surface")
(text "Go")))))
(script
(text "async function use_password(event) {
event.preventDefault();
const hash = Array.from(new Uint8Array(await window.crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(event.target.password.value))));
const hex_hash = hash.map((b) => b.toString(16).padStart(2, \"0\")).join(\"\");
window.location.href = `?key=h:${hex_hash}`;
}"))
(text "{% endblock %}")

View file

@ -24,7 +24,7 @@
("class" "w-full flex justify-between gap-2")
(a
("class" "button")
("href" "/{{ entry.slug }}/edit")
("href" "/{{ entry.slug }}/edit{% if password -%} ?key={{ password }} {%- endif %}")
(text "Edit"))
(div

View file

@ -95,6 +95,15 @@ pub struct EntryMetadata {
/// If this entry shows up in search engines.
#[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
pub option_disable_search_engine: bool,
/// The password that is required to view this entry.
#[serde(default, alias = "OPTION_VIEW_PASSWORD")]
pub option_view_password: String,
/// The password that is required to view the source of the entry.
///
/// If no password is provided but a view password IS provided, the view
/// password will be used.
#[serde(default, alias = "OPTION_SOURCE_PASSWORD")]
pub option_source_password: String,
/// The theme that is automatically used when this entry is viewed.
#[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
pub access_recommended_theme: RecommendedTheme,
@ -388,6 +397,17 @@ macro_rules! metadata_css {
}
};
($selector:expr, $property:literal !important, $field:ident->$output:ident) => {
if !$field.is_empty() {
$output.push_str(&format!(
"{} {{ {}: {} !important; }}\n",
$selector,
$property,
EntryMetadata::css_escape(&$field)
));
}
};
($selector:expr, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
if !$self.$field.is_empty() {
$output.push_str(&format!(
@ -404,7 +424,7 @@ macro_rules! text_size {
($selector:literal, $split:ident, $idx:literal, $output:ident) => {
if let Some(x) = $split.get($idx) {
if *x != "default" && *x != "0" {
metadata_css!($selector, "font-size", x->$output);
metadata_css!($selector, "font-size" !important, x->$output);
}
}
}

View file

@ -6,7 +6,7 @@ use crate::{
};
use axum::{
Extension, Json, Router,
extract::Path,
extract::{Path, Query},
http::{HeaderMap, HeaderValue},
response::{Html, IntoResponse},
routing::{get, get_service, post},
@ -119,9 +119,16 @@ async fn view_doc_request(
return Html(tera.render("doc.lisp", &ctx).unwrap());
}
#[derive(Deserialize)]
pub struct ViewQuery {
#[serde(default)]
pub key: String,
}
async fn view_request(
Extension(data): Extension<State>,
Path(slug): Path<String>,
Query(props): Query<ViewQuery>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let entry = match data
@ -163,6 +170,15 @@ async fn view_request(
return Html(tera.render("error.lisp", &ctx).unwrap());
}
// ...
if !metadata.option_view_password.is_empty()
&& metadata.option_view_password != props.key.clone()
{
let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry);
return Html(tera.render("password.lisp", &ctx).unwrap());
}
// pull views
let views = if !metadata.option_disable_views {
match data
@ -207,6 +223,7 @@ async fn view_request(
ctx.insert("metadata", &metadata);
ctx.insert("metadata_head", &metadata.head_tags());
ctx.insert("metadata_css", &metadata.css());
ctx.insert("password", &props.key);
Html(tera.render("view.lisp", &ctx).unwrap())
}
@ -214,6 +231,7 @@ async fn view_request(
async fn editor_request(
Extension(data): Extension<State>,
Path(slug): Path<String>,
Query(props): Query<ViewQuery>,
) -> impl IntoResponse {
let (ref data, ref tera, ref build_code) = *data.read().await;
let entry = match data
@ -238,9 +256,34 @@ async fn editor_request(
}
};
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&entry.metadata))
{
Ok(x) => x,
Err(e) => {
let mut ctx = default_context(&data, &build_code);
ctx.insert("error", &e.to_string());
return Html(tera.render("error.lisp", &ctx).unwrap());
}
};
// ...
if if !metadata.option_source_password.is_empty() {
metadata.option_source_password != props.key
} else if !metadata.option_view_password.is_empty() {
metadata.option_view_password != props.key
} else {
false
} {
let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry);
return Html(tera.render("password.lisp", &ctx).unwrap());
}
// ...
let mut ctx = default_context(&data, &build_code);
ctx.insert("entry", &entry);
ctx.insert("password", &props.key);
Html(tera.render("edit.lisp", &ctx).unwrap())
}
@ -311,6 +354,35 @@ fn default_random() -> String {
salt()
}
fn hash_passwords(metadata: &mut EntryMetadata) -> (bool, String) {
// hash passwords
let do_update_metadata = (!metadata.option_view_password.is_empty()
|| !metadata.option_source_password.is_empty())
&& (!metadata.option_view_password.starts_with("h:")
|| !metadata.option_source_password.starts_with("h:"));
if !metadata.option_view_password.is_empty() && !metadata.option_view_password.starts_with("h:")
{
metadata.option_view_password =
format!("h:{}", hash(metadata.option_view_password.clone()));
}
if !metadata.option_source_password.is_empty()
&& !metadata.option_source_password.starts_with("h:")
{
metadata.option_source_password =
format!("h:{}", hash(metadata.option_source_password.clone()));
}
if do_update_metadata {
if let Ok(x) = toml::to_string_pretty(&metadata) {
return (true, x);
};
}
(false, String::new())
}
/// The time that must be waited between each entry creation.
const CREATE_WAIT_TIME: usize = 5000;
@ -318,7 +390,7 @@ async fn create_request(
jar: CookieJar,
headers: HeaderMap,
Extension(data): Extension<State>,
Json(req): Json<CreateEntry>,
Json(mut req): Json<CreateEntry>,
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
let (ref data, _, _) = *data.read().await;
@ -382,7 +454,8 @@ async fn create_request(
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
let mut metadata: EntryMetadata =
match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
};
@ -391,6 +464,11 @@ async fn create_request(
return Err(Json(Error::MiscError(e.to_string()).into()));
}
let (do_update_metadata, updated) = hash_passwords(&mut metadata);
if do_update_metadata {
req.metadata = updated;
}
// check for existing
if data
.query(&SimplifiedQuery {
@ -471,7 +549,7 @@ async fn edit_request(
headers: HeaderMap,
Extension(data): Extension<State>,
Path(slug): Path<String>,
Json(req): Json<EditEntry>,
Json(mut req): Json<EditEntry>,
) -> impl IntoResponse {
let (ref data, _, _) = *data.read().await;
@ -500,7 +578,8 @@ async fn edit_request(
}
// check metadata
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
let mut metadata: EntryMetadata =
match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
};
@ -509,6 +588,11 @@ async fn edit_request(
return Json(Error::MiscError(e.to_string()).into());
}
let (do_update_metadata, updated) = hash_passwords(&mut metadata);
if do_update_metadata {
req.metadata = updated;
}
// ...
let (id, mut entry) = match data
.query(&SimplifiedQuery {