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
+93 -7
View File
@@ -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, &note).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),
}