guest password protect

This commit is contained in:
Mateusz Gruszczyński
2026-08-04 16:41:45 +02:00
parent 8d58549d11
commit 6fc408ddf7
18 changed files with 527 additions and 43 deletions
+59 -8
View File
@@ -18,6 +18,11 @@ pub struct CreatePadRequest {
content: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct SetPadPasswordRequest {
password: String,
}
#[derive(Debug, Serialize)]
pub struct CreatePadResponse {
slug: String,
@@ -51,6 +56,7 @@ pub struct PadInfo {
personal_editor_settings: bool,
can_save_editor_settings: bool,
can_manage_authorship: bool,
can_set_password: bool,
files: Vec<MarkdownFileReference>,
}
@@ -134,6 +140,7 @@ pub async fn pad_info(
let can_save_editor_settings = (personal_editor_settings || can_manage_authorship)
&& has_write_permission(&state, &headers, "pad", &slug).await?;
if pad.is_private == 0
&& pad.password_hash.is_some()
&& !db::pad_public_page_disabled(&state.db, pad.id).await?
&& !db::pad_public_page_enabled(&state.db, pad.id).await?
{
@@ -165,10 +172,36 @@ pub async fn pad_info(
personal_editor_settings,
can_save_editor_settings,
can_manage_authorship,
can_set_password: pad.password_hash.is_none() && (account_owner || guest_owner),
files: markdown_file_references(&state, Some(pad.id), None, None).await?,
}))
}
pub async fn set_pad_password(
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<SetPadPasswordRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let pad = db::find_pad(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
if pad.password_hash.is_some() {
return Err(ApiError::bad_request("This note already has a password."));
}
let account_owner = crate::auth::is_resource_owner(
&state, "pad", &slug, user_session_token(&headers),
).await.unwrap_or(false);
let guest_owner = pad_creator_is_requester(&headers, &pad);
if !account_owner && !guest_owner {
return Err(ApiError::forbidden("Only the note owner can set its password."));
}
let password = validate_password(Some(payload.password.as_str()))?
.ok_or_else(|| ApiError::bad_request("Password is required."))?;
db::set_pad_password(&state.db, &slug, password).await?;
Ok(Json(serde_json::json!({"ok": true, "protected": true})))
}
pub async fn set_pad_editor_settings(
State(state): State<SharedState>,
headers: HeaderMap,
@@ -279,6 +312,11 @@ pub async fn publish_pad_page(
};
require_write(level)?;
let enabled = payload.enabled.unwrap_or(true);
if enabled && pad.password_hash.is_none() {
return Err(ApiError::bad_request(
"Set a resource password before enabling the published page.",
));
}
if !enabled {
db::unpublish_pad(&state.db, pad.id).await?;
db::set_pad_public_page_disabled(&state.db, pad.id, true).await?;
@@ -340,6 +378,11 @@ pub async fn publish_note_page(
};
require_write(level)?;
let enabled = payload.enabled.unwrap_or(true);
if enabled && workspace.password_hash.is_none() {
return Err(ApiError::bad_request(
"Set a workspace password before enabling the published page.",
));
}
if !enabled {
db::unpublish_note(&state.db, note.id).await?;
db::set_note_public_page_disabled(&state.db, note.id, true).await?;
@@ -373,9 +416,7 @@ async fn ensure_public_page_access(
) -> Result<(), ApiError> {
let password = page_password(headers);
if let Some(pad_id) = page.pad_id {
if db::pad_public_page_unprotected(&state.db, pad_id).await? {
return Ok(());
}
let page_unprotected = db::pad_public_page_unprotected(&state.db, pad_id).await?;
let sql = match state.db.kind() {
crate::database::DatabaseKind::Postgres => "SELECT slug FROM pads WHERE id = $1",
_ => "SELECT slug FROM pads WHERE id = ?",
@@ -390,7 +431,12 @@ async fn ensure_public_page_access(
let pad = db::find_pad(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
if has_header_resource_access(state, headers, "pad", &slug).await? {
if pad.password_hash.is_none() {
return Err(ApiError::forbidden(
"This published page is unavailable until a resource password is set.",
));
}
if page_unprotected || has_header_resource_access(state, headers, "pad", &slug).await? {
return Ok(());
}
let password_ok = db::verify_pad_password(&pad, password);
@@ -406,9 +452,7 @@ async fn ensure_public_page_access(
};
}
if let Some(note_id) = page.note_id {
if db::note_public_page_unprotected(&state.db, note_id).await? {
return Ok(());
}
let page_unprotected = db::note_public_page_unprotected(&state.db, note_id).await?;
let sql = match state.db.kind() {
crate::database::DatabaseKind::Postgres => {
"SELECT w.slug FROM notes n JOIN workspaces w ON w.id = n.workspace_id WHERE n.id = $1"
@@ -427,7 +471,14 @@ async fn ensure_public_page_access(
let workspace = db::find_workspace(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_workspace)?;
if has_header_resource_access(state, headers, "workspace", &slug).await? {
if workspace.password_hash.is_none() {
return Err(ApiError::forbidden(
"This published page is unavailable until a workspace password is set.",
));
}
if page_unprotected
|| has_header_resource_access(state, headers, "workspace", &slug).await?
{
return Ok(());
}
let password_ok = db::verify_workspace_password(&workspace, password);