guest password protect
This commit is contained in:
+93
-7
@@ -114,6 +114,33 @@ fn pad_creator_is_requester(headers: &HeaderMap, pad: &db::Pad) -> bool {
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
}
|
||||
|
||||
fn workspace_creator_is_requester(headers: &HeaderMap, workspace: &db::Workspace) -> bool {
|
||||
workspace
|
||||
.created_by_guest_id
|
||||
.as_deref()
|
||||
.zip(requester_guest_id(headers))
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
}
|
||||
|
||||
async fn can_set_workspace_password(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
workspace: &db::Workspace,
|
||||
) -> bool {
|
||||
if workspace.password_hash.is_some() {
|
||||
return false;
|
||||
}
|
||||
let account_owner = crate::auth::is_resource_owner(
|
||||
state,
|
||||
"workspace",
|
||||
&workspace.slug,
|
||||
user_session_token(headers),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
account_owner || workspace_creator_is_requester(headers, workspace)
|
||||
}
|
||||
|
||||
async fn has_write_permission(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
@@ -258,6 +285,11 @@ pub struct CreateNoteRequest {
|
||||
created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetWorkspacePasswordRequest {
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RestoreRequest {
|
||||
#[serde(default)]
|
||||
@@ -273,6 +305,7 @@ pub struct WorkspaceInfo {
|
||||
title: String,
|
||||
protected: bool,
|
||||
access_level: String,
|
||||
can_set_password: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
}
|
||||
@@ -360,6 +393,7 @@ pub struct NoteInfo {
|
||||
personal_editor_settings: bool,
|
||||
can_save_editor_settings: bool,
|
||||
can_manage_authorship: bool,
|
||||
can_set_password: bool,
|
||||
files: Vec<MarkdownFileReference>,
|
||||
}
|
||||
|
||||
@@ -536,11 +570,23 @@ pub async fn create_workspace(
|
||||
let password = validate_password(payload.password.as_deref())?;
|
||||
let slug = unique_workspace_slug(&state, title).await?;
|
||||
|
||||
let workspace = db::create_workspace(&state.db, &slug, title, password).await?;
|
||||
if let Some(user) = crate::auth::optional_user(&state, &headers)
|
||||
let account_user = crate::auth::optional_user(&state, &headers)
|
||||
.await
|
||||
.map_err(|e| ApiError::forbidden(&e.message))?
|
||||
{
|
||||
.map_err(|e| ApiError::forbidden(&e.message))?;
|
||||
let created_by_guest_id = if account_user.is_none() {
|
||||
requester_guest_id(&headers)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let workspace = db::create_workspace(
|
||||
&state.db,
|
||||
&slug,
|
||||
title,
|
||||
password,
|
||||
created_by_guest_id,
|
||||
)
|
||||
.await?;
|
||||
if let Some(user) = account_user {
|
||||
sqlx::query(queries::get(
|
||||
state.db.kind(),
|
||||
queries::USER_ATTACH_WORKSPACE,
|
||||
@@ -585,7 +631,37 @@ pub async fn workspace_info(
|
||||
workspace.password_hash.is_some(),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(workspace_info_from(&workspace, access_level)))
|
||||
let can_set_password = can_set_workspace_password(&state, &headers, &workspace).await;
|
||||
Ok(Json(workspace_info_from(
|
||||
&workspace,
|
||||
access_level,
|
||||
can_set_password,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn set_workspace_password(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path(workspace_slug): Path<String>,
|
||||
Json(payload): Json<SetWorkspacePasswordRequest>,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let workspace = db::find_workspace(&state.db, &workspace_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
if workspace.password_hash.is_some() {
|
||||
return Err(ApiError::bad_request(
|
||||
"This workspace already has a password.",
|
||||
));
|
||||
}
|
||||
if !can_set_workspace_password(&state, &headers, &workspace).await {
|
||||
return Err(ApiError::forbidden(
|
||||
"Only the workspace 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_workspace_password(&state.db, &workspace_slug, password).await?;
|
||||
Ok(Json(serde_json::json!({"ok": true, "protected": true})))
|
||||
}
|
||||
|
||||
pub async fn open_workspace(
|
||||
@@ -668,8 +744,9 @@ pub async fn open_workspace(
|
||||
if db::verify_workspace_password(&workspace, payload.password.as_deref()) {
|
||||
access_level = AccessLevel::Write;
|
||||
}
|
||||
let can_set_password = can_set_workspace_password(&state, &headers, &workspace).await;
|
||||
Ok(Json(WorkspaceOpenResponse {
|
||||
workspace: workspace_info_from(&workspace, access_level),
|
||||
workspace: workspace_info_from(&workspace, access_level, can_set_password),
|
||||
notes,
|
||||
pagination: ListPaginationMeta {
|
||||
page,
|
||||
@@ -913,6 +990,7 @@ pub async fn note_info(
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let workspace_guest_owner = workspace_creator_is_requester(&headers, &workspace);
|
||||
let note_owner = note_creator_is_requester(&state, &headers, ¬e).await?;
|
||||
let password_write_access =
|
||||
has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
@@ -924,6 +1002,7 @@ pub async fn note_info(
|
||||
&& has_write_permission(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
|
||||
if workspace.is_private == 0
|
||||
&& workspace.password_hash.is_some()
|
||||
&& !db::note_public_page_disabled(&state.db, note.id).await?
|
||||
&& !db::note_public_page_enabled(&state.db, note.id).await?
|
||||
{
|
||||
@@ -958,6 +1037,8 @@ pub async fn note_info(
|
||||
personal_editor_settings,
|
||||
can_save_editor_settings,
|
||||
can_manage_authorship,
|
||||
can_set_password: workspace.password_hash.is_none()
|
||||
&& (workspace_owner || workspace_guest_owner),
|
||||
files: markdown_file_references(&state, None, Some(note.id), None).await?,
|
||||
}))
|
||||
}
|
||||
@@ -1408,12 +1489,17 @@ async fn authorized_note(
|
||||
Ok((workspace, note))
|
||||
}
|
||||
|
||||
fn workspace_info_from(workspace: &db::Workspace, access_level: AccessLevel) -> WorkspaceInfo {
|
||||
fn workspace_info_from(
|
||||
workspace: &db::Workspace,
|
||||
access_level: AccessLevel,
|
||||
can_set_password: bool,
|
||||
) -> WorkspaceInfo {
|
||||
WorkspaceInfo {
|
||||
slug: workspace.slug.clone(),
|
||||
title: workspace.title.clone(),
|
||||
protected: workspace.password_hash.is_some(),
|
||||
access_level: access_level_name(access_level).into(),
|
||||
can_set_password,
|
||||
created_at: db::normalize_timestamp(&workspace.created_at),
|
||||
updated_at: db::normalize_timestamp(&workspace.updated_at),
|
||||
}
|
||||
|
||||
+59
-8
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user