add: option_source_password and option_view_password
This commit is contained in:
parent
a49efdd238
commit
2de840f50d
5 changed files with 158 additions and 13 deletions
|
@ -180,6 +180,11 @@ video {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-nest .card:nth-child(1) {
|
||||||
|
background: var(--color-super-raised);
|
||||||
|
padding: var(--pad-2) var(--pad-4);
|
||||||
|
}
|
||||||
|
|
||||||
/* button */
|
/* button */
|
||||||
.button {
|
.button {
|
||||||
--h: 36px;
|
--h: 36px;
|
||||||
|
@ -307,6 +312,10 @@ input[data-invalid] {
|
||||||
border-left: inset 5px var(--color-red);
|
border-left: inset 5px var(--color-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input.surface {
|
||||||
|
background: var(--color-surface);
|
||||||
|
}
|
||||||
|
|
||||||
/* typo */
|
/* typo */
|
||||||
p {
|
p {
|
||||||
margin-bottom: var(--pad-4);
|
margin-bottom: var(--pad-4);
|
||||||
|
|
32
app/templates_src/password.lisp
Normal file
32
app/templates_src/password.lisp
Normal 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 %}")
|
|
@ -24,7 +24,7 @@
|
||||||
("class" "w-full flex justify-between gap-2")
|
("class" "w-full flex justify-between gap-2")
|
||||||
(a
|
(a
|
||||||
("class" "button")
|
("class" "button")
|
||||||
("href" "/{{ entry.slug }}/edit")
|
("href" "/{{ entry.slug }}/edit{% if password -%} ?key={{ password }} {%- endif %}")
|
||||||
(text "Edit"))
|
(text "Edit"))
|
||||||
|
|
||||||
(div
|
(div
|
||||||
|
|
22
src/model.rs
22
src/model.rs
|
@ -95,6 +95,15 @@ pub struct EntryMetadata {
|
||||||
/// If this entry shows up in search engines.
|
/// If this entry shows up in search engines.
|
||||||
#[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
|
#[serde(default, alias = "OPTION_DISABLE_SEARCH_ENGINE")]
|
||||||
pub option_disable_search_engine: bool,
|
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.
|
/// The theme that is automatically used when this entry is viewed.
|
||||||
#[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
|
#[serde(default, alias = "ACCESS_RECOMMENDED_THEME")]
|
||||||
pub access_recommended_theme: RecommendedTheme,
|
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) => {
|
($selector:expr, $property:literal, $format:literal, $self:ident.$field:ident->$output:ident) => {
|
||||||
if !$self.$field.is_empty() {
|
if !$self.$field.is_empty() {
|
||||||
$output.push_str(&format!(
|
$output.push_str(&format!(
|
||||||
|
@ -404,7 +424,7 @@ macro_rules! text_size {
|
||||||
($selector:literal, $split:ident, $idx:literal, $output:ident) => {
|
($selector:literal, $split:ident, $idx:literal, $output:ident) => {
|
||||||
if let Some(x) = $split.get($idx) {
|
if let Some(x) = $split.get($idx) {
|
||||||
if *x != "default" && *x != "0" {
|
if *x != "default" && *x != "0" {
|
||||||
metadata_css!($selector, "font-size", x->$output);
|
metadata_css!($selector, "font-size" !important, x->$output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
106
src/routes.rs
106
src/routes.rs
|
@ -6,7 +6,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
Extension, Json, Router,
|
Extension, Json, Router,
|
||||||
extract::Path,
|
extract::{Path, Query},
|
||||||
http::{HeaderMap, HeaderValue},
|
http::{HeaderMap, HeaderValue},
|
||||||
response::{Html, IntoResponse},
|
response::{Html, IntoResponse},
|
||||||
routing::{get, get_service, post},
|
routing::{get, get_service, post},
|
||||||
|
@ -119,9 +119,16 @@ async fn view_doc_request(
|
||||||
return Html(tera.render("doc.lisp", &ctx).unwrap());
|
return Html(tera.render("doc.lisp", &ctx).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ViewQuery {
|
||||||
|
#[serde(default)]
|
||||||
|
pub key: String,
|
||||||
|
}
|
||||||
|
|
||||||
async fn view_request(
|
async fn view_request(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
|
Query(props): Query<ViewQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
let entry = match data
|
let entry = match data
|
||||||
|
@ -163,6 +170,15 @@ async fn view_request(
|
||||||
return Html(tera.render("error.lisp", &ctx).unwrap());
|
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
|
// pull views
|
||||||
let views = if !metadata.option_disable_views {
|
let views = if !metadata.option_disable_views {
|
||||||
match data
|
match data
|
||||||
|
@ -207,6 +223,7 @@ async fn view_request(
|
||||||
ctx.insert("metadata", &metadata);
|
ctx.insert("metadata", &metadata);
|
||||||
ctx.insert("metadata_head", &metadata.head_tags());
|
ctx.insert("metadata_head", &metadata.head_tags());
|
||||||
ctx.insert("metadata_css", &metadata.css());
|
ctx.insert("metadata_css", &metadata.css());
|
||||||
|
ctx.insert("password", &props.key);
|
||||||
|
|
||||||
Html(tera.render("view.lisp", &ctx).unwrap())
|
Html(tera.render("view.lisp", &ctx).unwrap())
|
||||||
}
|
}
|
||||||
|
@ -214,6 +231,7 @@ async fn view_request(
|
||||||
async fn editor_request(
|
async fn editor_request(
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
|
Query(props): Query<ViewQuery>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let (ref data, ref tera, ref build_code) = *data.read().await;
|
let (ref data, ref tera, ref build_code) = *data.read().await;
|
||||||
let entry = match data
|
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);
|
let mut ctx = default_context(&data, &build_code);
|
||||||
|
|
||||||
ctx.insert("entry", &entry);
|
ctx.insert("entry", &entry);
|
||||||
|
ctx.insert("password", &props.key);
|
||||||
|
|
||||||
Html(tera.render("edit.lisp", &ctx).unwrap())
|
Html(tera.render("edit.lisp", &ctx).unwrap())
|
||||||
}
|
}
|
||||||
|
@ -311,6 +354,35 @@ fn default_random() -> String {
|
||||||
salt()
|
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.
|
/// The time that must be waited between each entry creation.
|
||||||
const CREATE_WAIT_TIME: usize = 5000;
|
const CREATE_WAIT_TIME: usize = 5000;
|
||||||
|
|
||||||
|
@ -318,7 +390,7 @@ async fn create_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Json(req): Json<CreateEntry>,
|
Json(mut req): Json<CreateEntry>,
|
||||||
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
|
) -> std::result::Result<impl IntoResponse, Json<ApiReturn<()>>> {
|
||||||
let (ref data, _, _) = *data.read().await;
|
let (ref data, _, _) = *data.read().await;
|
||||||
|
|
||||||
|
@ -382,15 +454,21 @@ async fn create_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
// check metadata
|
// check metadata
|
||||||
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
|
let mut metadata: EntryMetadata =
|
||||||
Ok(x) => x,
|
match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
|
||||||
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
|
Ok(x) => x,
|
||||||
};
|
Err(e) => return Err(Json(Error::MiscError(e.to_string()).into())),
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(e) = metadata.validate() {
|
if let Err(e) = metadata.validate() {
|
||||||
return Err(Json(Error::MiscError(e.to_string()).into()));
|
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
|
// check for existing
|
||||||
if data
|
if data
|
||||||
.query(&SimplifiedQuery {
|
.query(&SimplifiedQuery {
|
||||||
|
@ -471,7 +549,7 @@ async fn edit_request(
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Path(slug): Path<String>,
|
Path(slug): Path<String>,
|
||||||
Json(req): Json<EditEntry>,
|
Json(mut req): Json<EditEntry>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let (ref data, _, _) = *data.read().await;
|
let (ref data, _, _) = *data.read().await;
|
||||||
|
|
||||||
|
@ -500,15 +578,21 @@ async fn edit_request(
|
||||||
}
|
}
|
||||||
|
|
||||||
// check metadata
|
// check metadata
|
||||||
let metadata: EntryMetadata = match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
|
let mut metadata: EntryMetadata =
|
||||||
Ok(x) => x,
|
match toml::from_str(&EntryMetadata::ini_to_toml(&req.metadata)) {
|
||||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
Ok(x) => x,
|
||||||
};
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
|
};
|
||||||
|
|
||||||
if let Err(e) = metadata.validate() {
|
if let Err(e) = metadata.validate() {
|
||||||
return Json(Error::MiscError(e.to_string()).into());
|
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
|
let (id, mut entry) = match data
|
||||||
.query(&SimplifiedQuery {
|
.query(&SimplifiedQuery {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue