v0.2.96
This commit is contained in:
1 parent
b15a6c54ce
commit
d36a4b2451
22 files changed
+926
-114
No files matched your search
+118
-17
@@ -193,6 +193,25 @@ async fn can_manage_workspace_settings(
|
||||
account_owner || workspace_creator_is_requester(headers, workspace)
|
||||
}
|
||||
|
||||
async fn has_explicit_write_permission(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
supplied_access_token: Option<&str>,
|
||||
) -> Result<bool, ApiError> {
|
||||
Ok(request_access_level(
|
||||
state,
|
||||
headers,
|
||||
kind,
|
||||
slug,
|
||||
supplied_access_token,
|
||||
bearer_token(headers),
|
||||
)
|
||||
.await?
|
||||
>= AccessLevel::Write)
|
||||
}
|
||||
|
||||
async fn has_write_permission(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
@@ -363,6 +382,21 @@ pub struct CreateNoteRequest {
|
||||
created_by: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RenameRequest {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
#[serde(default)]
|
||||
access_token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RenameResponse {
|
||||
title: String,
|
||||
updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetWorkspacePasswordRequest {
|
||||
password: String,
|
||||
@@ -439,6 +473,7 @@ fn normalize_list_per_page(value: usize) -> usize {
|
||||
pub struct NoteListItem {
|
||||
slug: String,
|
||||
title: String,
|
||||
can_rename: bool,
|
||||
created_at: String,
|
||||
updated_at: String,
|
||||
url: String,
|
||||
@@ -469,6 +504,7 @@ pub struct NoteInfo {
|
||||
workspace_title: String,
|
||||
slug: String,
|
||||
title: String,
|
||||
can_rename: bool,
|
||||
protected: bool,
|
||||
access_level: String,
|
||||
note_protected: bool,
|
||||
@@ -695,14 +731,8 @@ pub async fn create_workspace(
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let workspace = db::create_workspace(
|
||||
&state.db,
|
||||
&slug,
|
||||
title,
|
||||
password,
|
||||
created_by_guest_id,
|
||||
)
|
||||
.await?;
|
||||
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(),
|
||||
@@ -772,12 +802,8 @@ pub async fn set_workspace_settings(
|
||||
"Only the workspace owner can change workspace settings.",
|
||||
));
|
||||
}
|
||||
db::set_workspace_archive_deleted_notes(
|
||||
&state.db,
|
||||
workspace.id,
|
||||
payload.archive_deleted_notes,
|
||||
)
|
||||
.await?;
|
||||
db::set_workspace_archive_deleted_notes(&state.db, workspace.id, payload.archive_deleted_notes)
|
||||
.await?;
|
||||
let workspace = db::find_workspace(&state.db, &workspace_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_workspace)?;
|
||||
@@ -870,8 +896,7 @@ pub async fn set_workspace_password(
|
||||
"Only the workspace owner can set its password.",
|
||||
));
|
||||
}
|
||||
let except_client_id =
|
||||
crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
let except_client_id = crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
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?;
|
||||
@@ -902,6 +927,21 @@ pub async fn open_workspace(
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let password_write_access =
|
||||
db::verify_workspace_password(&workspace, payload.password.as_deref());
|
||||
let explicit_write_access = has_explicit_write_permission(
|
||||
&state,
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace.slug,
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let mut access_level = effective_header_access_level(
|
||||
&state,
|
||||
&headers,
|
||||
@@ -911,7 +951,7 @@ pub async fn open_workspace(
|
||||
workspace.password_hash.is_some(),
|
||||
)
|
||||
.await?;
|
||||
if db::verify_workspace_password(&workspace, payload.password.as_deref()) {
|
||||
if password_write_access {
|
||||
access_level = AccessLevel::Write;
|
||||
}
|
||||
let workspace_account_owner = crate::auth::is_resource_owner(
|
||||
@@ -981,6 +1021,10 @@ pub async fn open_workspace(
|
||||
url: format!("/w/{}/n/{}", workspace.slug, note.slug),
|
||||
slug: note.slug,
|
||||
title: note.title,
|
||||
can_rename: workspace_owner
|
||||
|| note_owner
|
||||
|| password_write_access
|
||||
|| explicit_write_access,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
protected: note.protected,
|
||||
@@ -1118,6 +1162,7 @@ pub async fn create_note(
|
||||
url: format!("/w/{workspace_slug}/n/{slug}"),
|
||||
slug: note.slug,
|
||||
title: note.title,
|
||||
can_rename: true,
|
||||
created_at: db::normalize_timestamp(¬e.created_at),
|
||||
updated_at: db::normalize_timestamp(¬e.updated_at),
|
||||
protected: note.protected,
|
||||
@@ -1132,6 +1177,59 @@ pub async fn create_note(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn rename_note(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path((workspace_slug, note_slug)): Path<(String, String)>,
|
||||
Json(payload): Json<RenameRequest>,
|
||||
) -> Result<Json<RenameResponse>, ApiError> {
|
||||
let (workspace, note) = authorized_note(
|
||||
&state,
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let requester_is_owner = requester_owns_note(&state, &headers, &workspace, ¬e).await?;
|
||||
let password_write_access =
|
||||
db::verify_workspace_password(&workspace, payload.password.as_deref());
|
||||
let explicit_write_access = has_explicit_write_permission(
|
||||
&state,
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
if !requester_is_owner && !password_write_access && !explicit_write_access {
|
||||
return Err(ApiError::forbidden(
|
||||
"Only the note owner or a user with write permission can rename it.",
|
||||
));
|
||||
}
|
||||
let title = validate_name(&payload.name, "Note name")?;
|
||||
db::rename_note(&state.db, note.id, workspace.id, title).await?;
|
||||
let updated = db::find_note(&state.db, workspace.id, ¬e_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
Ok(Json(RenameResponse {
|
||||
title: updated.title,
|
||||
updated_at: db::normalize_timestamp(&updated.updated_at),
|
||||
}))
|
||||
}
|
||||
|
||||
fn clean_editor_color(value: Option<&str>) -> Result<Option<String>, ApiError> {
|
||||
let Some(value) = value else {
|
||||
return Ok(None);
|
||||
@@ -1263,6 +1361,8 @@ pub async fn note_info(
|
||||
let password_write_access =
|
||||
has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
let requester_is_owner = workspace_owner || workspace_guest_owner || note_owner;
|
||||
let explicit_rename_access =
|
||||
has_explicit_write_permission(&state, &headers, "workspace", &workspace_slug, None).await?;
|
||||
let can_manage_authorship = can_manage_resource_settings(
|
||||
workspace_owner || workspace_guest_owner,
|
||||
note_owner,
|
||||
@@ -1288,6 +1388,7 @@ pub async fn note_info(
|
||||
workspace_title: workspace.title,
|
||||
slug: note.slug,
|
||||
title: note.title,
|
||||
can_rename: requester_is_owner || explicit_rename_access,
|
||||
protected: workspace.password_hash.is_some(),
|
||||
access_level: access_level_name(access_level).into(),
|
||||
note_protected: note.protected,
|
||||
|
||||
+53
-6
@@ -35,6 +35,7 @@ pub struct CreatePadResponse {
|
||||
pub struct PadInfo {
|
||||
slug: String,
|
||||
title: String,
|
||||
can_rename: bool,
|
||||
protected: bool,
|
||||
access_level: String,
|
||||
allow_public_task_updates: bool,
|
||||
@@ -139,6 +140,8 @@ pub async fn pad_info(
|
||||
.unwrap_or(false);
|
||||
let guest_owner = pad_creator_is_requester(&headers, &pad);
|
||||
let password_write_access = has_password_write_access(&state, &headers, "pad", &slug).await?;
|
||||
let explicit_rename_access =
|
||||
has_explicit_write_permission(&state, &headers, "pad", &slug, None).await?;
|
||||
let can_manage_authorship =
|
||||
can_manage_resource_settings(account_owner, guest_owner, password_write_access);
|
||||
let upload_max_size_bytes = resource_upload_limit(&state, &headers, "pad", &slug).await?;
|
||||
@@ -155,6 +158,7 @@ pub async fn pad_info(
|
||||
Ok(Json(PadInfo {
|
||||
slug: pad.slug,
|
||||
title: pad.title,
|
||||
can_rename: guest_owner || explicit_rename_access,
|
||||
protected: pad.password_hash.is_some(),
|
||||
access_level: access_level_name(access_level).into(),
|
||||
allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?,
|
||||
@@ -190,6 +194,47 @@ pub async fn pad_info(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn rename_pad(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
Path(slug): Path<String>,
|
||||
Json(payload): Json<RenameRequest>,
|
||||
) -> Result<Json<RenameResponse>, ApiError> {
|
||||
let pad = authorized_pad(
|
||||
&state,
|
||||
&slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let guest_owner = pad_creator_is_requester(&headers, &pad);
|
||||
let password_write_access = db::verify_pad_password(&pad, payload.password.as_deref());
|
||||
let explicit_write_access = has_explicit_write_permission(
|
||||
&state,
|
||||
&headers,
|
||||
"pad",
|
||||
&slug,
|
||||
resource_request_token(&headers, "pad", &slug, payload.access_token.as_deref()),
|
||||
)
|
||||
.await?;
|
||||
if !guest_owner && !password_write_access && !explicit_write_access {
|
||||
return Err(ApiError::forbidden(
|
||||
"Only the note owner or a user with write permission can rename it.",
|
||||
));
|
||||
}
|
||||
let title = validate_name(&payload.name, "Note name")?;
|
||||
db::rename_pad(&state.db, pad.id, title).await?;
|
||||
let updated = db::find_pad(&state.db, &slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
Ok(Json(RenameResponse {
|
||||
title: updated.title,
|
||||
updated_at: db::normalize_timestamp(&updated.updated_at),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn set_pad_password(
|
||||
State(state): State<SharedState>,
|
||||
headers: HeaderMap,
|
||||
@@ -202,15 +247,17 @@ pub async fn set_pad_password(
|
||||
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 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 !can_set_resource_password(pad.password_hash.is_some(), account_owner, guest_owner) {
|
||||
return Err(ApiError::forbidden("Only the note owner can set its password."));
|
||||
return Err(ApiError::forbidden(
|
||||
"Only the note owner can set its password.",
|
||||
));
|
||||
}
|
||||
let except_client_id =
|
||||
crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
let except_client_id = crate::websocket::clean_collaboration_client_id(payload.client_id);
|
||||
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?;
|
||||
|
||||
+11
-8
@@ -161,6 +161,7 @@ pub fn router(
|
||||
.route("/api/public/{token}/tasks", post(api::update_public_task))
|
||||
.route("/api/pads", post(api::create_pad))
|
||||
.route("/api/pads/{slug}", get(api::pad_info))
|
||||
.route("/api/pads/{slug}/rename", post(api::rename_pad))
|
||||
.route("/api/pads/{slug}/history", post(api::pad_history))
|
||||
.route(
|
||||
"/api/pads/{slug}/editor-color",
|
||||
@@ -211,6 +212,10 @@ pub fn router(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}",
|
||||
get(api::note_info).delete(api::delete_note),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/rename",
|
||||
post(api::rename_note),
|
||||
)
|
||||
.route(
|
||||
"/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-color",
|
||||
get(api::note_editor_color).post(api::set_note_editor_color),
|
||||
@@ -287,10 +292,9 @@ pub fn files_router(state: SharedState) -> Router {
|
||||
|
||||
async fn language_catalog() -> Response {
|
||||
let mut response = Json(crate::i18n::metadata()).into_response();
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=300"),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||
response
|
||||
}
|
||||
|
||||
@@ -307,10 +311,9 @@ async fn language_bundle(Path(code): Path<String>) -> Response {
|
||||
header::CONTENT_TYPE,
|
||||
HeaderValue::from_static("application/json; charset=utf-8"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::CACHE_CONTROL,
|
||||
HeaderValue::from_static("public, max-age=86400"),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::CACHE_CONTROL, HeaderValue::from_static("no-cache"));
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -181,7 +181,9 @@ impl Config {
|
||||
"GUEST_UPLOAD_MAX_SIZE_MB",
|
||||
guest_upload_max_size_mb,
|
||||
)?,
|
||||
asset_version: format!("{}", env!("CARGO_PKG_VERSION")),
|
||||
asset_version: option_env!("RUSTPAD_ASSET_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION"))
|
||||
.to_string(),
|
||||
http_compression: values.bool("HTTP_COMPRESSION", false)?,
|
||||
asset_cache_max_age_seconds: values
|
||||
.nonnegative_u64("ASSET_CACHE_MAX_AGE_SECONDS", 600)?,
|
||||
|
||||
@@ -442,6 +442,36 @@ pub async fn create_note(
|
||||
.ok_or(sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
pub async fn rename_note(
|
||||
pool: &Database,
|
||||
note_id: i64,
|
||||
workspace_id: i64,
|
||||
title: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let (note_query, workspace_query) = match pool.kind() {
|
||||
DatabaseKind::Postgres => (
|
||||
"UPDATE notes SET title = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2",
|
||||
"UPDATE workspaces SET updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $1",
|
||||
),
|
||||
DatabaseKind::Sqlite | DatabaseKind::MySql => (
|
||||
"UPDATE notes SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
"UPDATE workspaces SET updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
),
|
||||
};
|
||||
let mut tx = pool.pool().begin().await?;
|
||||
sqlx::query(note_query)
|
||||
.bind(title)
|
||||
.bind(note_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
sqlx::query(workspace_query)
|
||||
.bind(workspace_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
tx.commit().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn save_revision(
|
||||
pool: &Database,
|
||||
note_id: i64,
|
||||
@@ -650,6 +680,25 @@ pub async fn create_pad(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rename_pad(
|
||||
pool: &Database,
|
||||
pad_id: i64,
|
||||
title: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
let query = match pool.kind() {
|
||||
DatabaseKind::Postgres =>
|
||||
"UPDATE pads SET title = $1, updated_at = (CURRENT_TIMESTAMP::text) WHERE id = $2",
|
||||
DatabaseKind::Sqlite | DatabaseKind::MySql =>
|
||||
"UPDATE pads SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
|
||||
};
|
||||
sqlx::query(query)
|
||||
.bind(title)
|
||||
.bind(pad_id)
|
||||
.execute(pool.pool())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_pad_password(
|
||||
pool: &Database,
|
||||
slug: &str,
|
||||
|
||||
Reference in new issue
Block a user