tokens and more
This commit is contained in:
+84
-42
@@ -38,6 +38,7 @@ const MAX_NAME_LENGTH: usize = 80;
|
||||
const MIN_PASSWORD_LENGTH: usize = 8;
|
||||
const MAX_PASSWORD_LENGTH: usize = 128;
|
||||
const MIN_WORKSPACE_SLUG_LENGTH: usize = 6;
|
||||
const MAX_DOCUMENT_SIZE_BYTES: usize = 2_000_000;
|
||||
|
||||
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
|
||||
crate::security::session_token(headers)
|
||||
@@ -81,9 +82,9 @@ fn requester_guest_id(headers: &HeaderMap) -> Option<&str> {
|
||||
.map(str::trim)
|
||||
.filter(|value| {
|
||||
(16..=64).contains(&value.len())
|
||||
&& value
|
||||
.chars()
|
||||
.all(|character| character.is_ascii_alphanumeric() || matches!(character, '-' | '_'))
|
||||
&& value.chars().all(|character| {
|
||||
character.is_ascii_alphanumeric() || matches!(character, '-' | '_')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -109,9 +110,7 @@ fn pad_creator_is_requester(headers: &HeaderMap, pad: &db::Pad) -> bool {
|
||||
pad.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
|
||||
})
|
||||
.is_some_and(|(owner_guest_id, requester_guest_id)| owner_guest_id == requester_guest_id)
|
||||
}
|
||||
|
||||
async fn has_write_permission(
|
||||
@@ -126,7 +125,8 @@ async fn has_write_permission(
|
||||
}
|
||||
let authorization = authorization_token(headers);
|
||||
if authorization != resource
|
||||
&& external_token_access_level(state, kind, slug, authorization).await? >= AccessLevel::Write
|
||||
&& external_token_access_level(state, kind, slug, authorization).await?
|
||||
>= AccessLevel::Write
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
@@ -191,10 +191,7 @@ pub(crate) async fn markdown_file_references(
|
||||
})
|
||||
.map(|file| MarkdownFileReference {
|
||||
filename: file.filename,
|
||||
url: crate::file_urls::public_file_url(
|
||||
state.files_public_url.as_deref(),
|
||||
&file.url,
|
||||
),
|
||||
url: crate::file_urls::public_file_url(state.files_public_url.as_deref(), &file.url),
|
||||
mime_type: file.mime_type,
|
||||
})
|
||||
.collect())
|
||||
@@ -253,6 +250,8 @@ pub struct PublicTaskUpdateRequest {
|
||||
pub struct CreateNoteRequest {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
content: Option<String>,
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
#[serde(default)]
|
||||
access_token: Option<String>,
|
||||
@@ -385,14 +384,7 @@ async fn save_editor_settings(
|
||||
creator_can_manage_authorship: bool,
|
||||
payload: EditorSettingsRequest,
|
||||
) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
if !has_write_permission(
|
||||
state,
|
||||
headers,
|
||||
permission_kind,
|
||||
permission_slug,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !has_write_permission(state, headers, permission_kind, permission_slug).await? {
|
||||
return Err(ApiError::forbidden(
|
||||
"Read and write access is required to save editor preferences",
|
||||
));
|
||||
@@ -416,13 +408,13 @@ async fn save_editor_settings(
|
||||
let can_manage_authorship = if wants_global_update {
|
||||
creator_can_manage_authorship
|
||||
|| crate::auth::is_resource_owner(
|
||||
state,
|
||||
permission_kind,
|
||||
permission_slug,
|
||||
user_session_token(headers),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
state,
|
||||
permission_kind,
|
||||
permission_slug,
|
||||
user_session_token(headers),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
@@ -433,7 +425,10 @@ async fn save_editor_settings(
|
||||
}
|
||||
|
||||
let preferences = if wants_personal_update {
|
||||
let user_id = user.as_ref().expect("personal preferences require a user").id;
|
||||
let user_id = user
|
||||
.as_ref()
|
||||
.expect("personal preferences require a user")
|
||||
.id;
|
||||
let mut preferences = db::load_editor_preferences(&state.db, user_id, resource)
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
@@ -467,12 +462,8 @@ async fn save_editor_settings(
|
||||
};
|
||||
|
||||
let resource_settings = if wants_global_update {
|
||||
let mut settings = db::load_resource_editor_settings(
|
||||
&state.db,
|
||||
settings_kind,
|
||||
settings_slug,
|
||||
)
|
||||
.await?;
|
||||
let mut settings =
|
||||
db::load_resource_editor_settings(&state.db, settings_kind, settings_slug).await?;
|
||||
if let Some(mode) = payload.authorship_mode {
|
||||
settings.authorship_mode = match mode.as_str() {
|
||||
"simple" => "simple".into(),
|
||||
@@ -567,7 +558,12 @@ pub async fn open_workspace(
|
||||
&state,
|
||||
&workspace_slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
@@ -614,7 +610,12 @@ pub async fn create_note(
|
||||
&state,
|
||||
&workspace_slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
@@ -628,13 +629,19 @@ pub async fn create_note(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
};
|
||||
require_write(level)?;
|
||||
let title = validate_name(&payload.name, "Note name")?;
|
||||
let initial_content = validate_initial_content(payload.content.as_deref())?;
|
||||
let base = slugify(title);
|
||||
if base.is_empty() {
|
||||
return Err(ApiError::bad_request(
|
||||
@@ -670,6 +677,17 @@ pub async fn create_note(
|
||||
created_by_guest_id.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
if let Some(content) = initial_content {
|
||||
db::save_revision(
|
||||
&state.db,
|
||||
note.id,
|
||||
workspace.id,
|
||||
content,
|
||||
created_by.as_deref(),
|
||||
"[]",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(NoteListItem {
|
||||
@@ -865,9 +883,8 @@ pub async fn set_note_editor_settings(
|
||||
let note = db::find_note(&state.db, workspace.id, ¬e_slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
let creator_can_manage_authorship =
|
||||
note_creator_is_requester(&state, &headers, ¬e).await?
|
||||
|| has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
let creator_can_manage_authorship = note_creator_is_requester(&state, &headers, ¬e).await?
|
||||
|| has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
|
||||
save_editor_settings(
|
||||
&state,
|
||||
&headers,
|
||||
@@ -893,7 +910,12 @@ pub async fn history(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
@@ -921,7 +943,12 @@ pub async fn restore(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
payload.password.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
@@ -935,7 +962,12 @@ pub async fn restore(
|
||||
&state,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, payload.access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
payload.access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -1238,6 +1270,16 @@ fn validate_password(password: Option<&str>) -> Result<Option<&str>, ApiError> {
|
||||
Ok(Some(password))
|
||||
}
|
||||
|
||||
fn validate_initial_content(content: Option<&str>) -> Result<Option<&str>, ApiError> {
|
||||
let Some(content) = content.filter(|value| !value.is_empty()) else {
|
||||
return Ok(None);
|
||||
};
|
||||
if content.len() > MAX_DOCUMENT_SIZE_BYTES {
|
||||
return Err(ApiError::bad_request("The document is too large"));
|
||||
}
|
||||
Ok(Some(content))
|
||||
}
|
||||
|
||||
async fn unique_workspace_slug(state: &SharedState, title: &str) -> Result<String, ApiError> {
|
||||
let base = slugify(title);
|
||||
if base.is_empty() {
|
||||
|
||||
+24
-56
@@ -14,6 +14,8 @@ pub struct CreatePadRequest {
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
password: Option<String>,
|
||||
#[serde(default)]
|
||||
content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -58,6 +60,7 @@ pub async fn create_pad(
|
||||
) -> Result<(StatusCode, Json<CreatePadResponse>), ApiError> {
|
||||
let title = validate_name(&payload.name, "Note name")?;
|
||||
let password = validate_password(payload.password.as_deref())?;
|
||||
let initial_content = validate_initial_content(payload.content.as_deref())?;
|
||||
let base = slugify(title);
|
||||
if base.is_empty() {
|
||||
return Err(ApiError::bad_request(
|
||||
@@ -68,26 +71,23 @@ pub async fn create_pad(
|
||||
let account_user = crate::auth::optional_user(&state, &headers)
|
||||
.await
|
||||
.map_err(|e| ApiError::forbidden(&e.message))?;
|
||||
let author = account_user.as_ref().map(|user| user.nickname.clone());
|
||||
let created_by_guest_id = if account_user.is_none() {
|
||||
requester_guest_id(&headers)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pad = db::create_pad(
|
||||
&state.db,
|
||||
&slug,
|
||||
title,
|
||||
password,
|
||||
created_by_guest_id,
|
||||
)
|
||||
.await?;
|
||||
if let Some(user) = account_user {
|
||||
let pad = db::create_pad(&state.db, &slug, title, password, created_by_guest_id).await?;
|
||||
if let Some(user) = account_user.as_ref() {
|
||||
sqlx::query(queries::get(state.db.kind(), queries::USER_ATTACH_PAD))
|
||||
.bind(user.id)
|
||||
.bind(&pad.slug)
|
||||
.execute(state.db.pool())
|
||||
.await?;
|
||||
}
|
||||
if let Some(content) = initial_content {
|
||||
db::save_pad_revision(&state.db, pad.id, content, author.as_deref(), "[]").await?;
|
||||
}
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(CreatePadResponse {
|
||||
@@ -105,31 +105,17 @@ pub async fn pad_info(
|
||||
let pad = db::find_pad(&state.db, &slug)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
ensure_private_resource_access(
|
||||
&state,
|
||||
&headers,
|
||||
"pad",
|
||||
&pad.slug,
|
||||
pad.is_private,
|
||||
)
|
||||
.await?;
|
||||
ensure_private_resource_access(&state, &headers, "pad", &pad.slug, pad.is_private).await?;
|
||||
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
|
||||
let (editor_preferences, personal_editor_settings) = user_editor_preferences(
|
||||
&state,
|
||||
&headers,
|
||||
db::EditorPreferenceResource::Pad(pad.id),
|
||||
)
|
||||
.await?;
|
||||
let (editor_preferences, personal_editor_settings) =
|
||||
user_editor_preferences(&state, &headers, db::EditorPreferenceResource::Pad(pad.id))
|
||||
.await?;
|
||||
let resource_editor_settings =
|
||||
db::load_resource_editor_settings(&state.db, "pad", &slug).await?;
|
||||
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);
|
||||
let password_write_access = has_password_write_access(&state, &headers, "pad", &slug).await?;
|
||||
let can_manage_authorship = account_owner || guest_owner || password_write_access;
|
||||
@@ -432,15 +418,8 @@ async fn ensure_public_page_access(
|
||||
return Ok(());
|
||||
}
|
||||
let password_ok = db::verify_workspace_password(&workspace, password);
|
||||
check_resource_password_attempt(
|
||||
state,
|
||||
headers,
|
||||
"workspace",
|
||||
&slug,
|
||||
password,
|
||||
password_ok,
|
||||
)
|
||||
.await?;
|
||||
check_resource_password_attempt(state, headers, "workspace", &slug, password, password_ok)
|
||||
.await?;
|
||||
if password_ok {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -462,13 +441,8 @@ pub async fn public_page(
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
ensure_public_page_access(&state, &headers, &page).await?;
|
||||
let files = markdown_file_references(
|
||||
&state,
|
||||
page.pad_id,
|
||||
page.note_id,
|
||||
Some(&page.content),
|
||||
)
|
||||
.await?;
|
||||
let files =
|
||||
markdown_file_references(&state, page.pad_id, page.note_id, Some(&page.content)).await?;
|
||||
Ok(Json(PublicPageResponse {
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
@@ -496,13 +470,8 @@ pub async fn update_public_task(
|
||||
let page = db::update_public_task(&state.db, &token, payload.source_line, payload.checked)
|
||||
.await?
|
||||
.ok_or_else(ApiError::not_found_note)?;
|
||||
let files = markdown_file_references(
|
||||
&state,
|
||||
page.pad_id,
|
||||
page.note_id,
|
||||
Some(&page.content),
|
||||
)
|
||||
.await?;
|
||||
let files =
|
||||
markdown_file_references(&state, page.pad_id, page.note_id, Some(&page.content)).await?;
|
||||
Ok(Json(PublicPageResponse {
|
||||
title: page.title,
|
||||
content: page.content,
|
||||
@@ -614,8 +583,7 @@ pub(super) async fn authorized_pad(
|
||||
}
|
||||
if pad.password_hash.is_some() && token_level < AccessLevel::Write {
|
||||
let password_ok = db::verify_pad_password(&pad, password);
|
||||
check_resource_password_attempt(state, headers, "pad", slug, password, password_ok)
|
||||
.await?;
|
||||
check_resource_password_attempt(state, headers, "pad", slug, password, password_ok).await?;
|
||||
if token_level == AccessLevel::None && !password_ok {
|
||||
return Err(ApiError::forbidden("Password required or incorrect."));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user