tokens and more

This commit is contained in:
Mateusz Gruszczyński
2026-08-01 00:15:37 +02:00
parent 6c5232ccc5
commit 1401054c71
18 changed files with 1966 additions and 285 deletions
+84 -42
View File
@@ -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, &note_slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
let creator_can_manage_authorship =
note_creator_is_requester(&state, &headers, &note).await?
|| has_password_write_access(&state, &headers, "workspace", &workspace_slug).await?;
let creator_can_manage_authorship = note_creator_is_requester(&state, &headers, &note).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,
&note_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,
&note_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
View File
@@ -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."));
}