v0.2.97
This commit is contained in:
1 parent
99a5700caa
commit
b752f52fd8
22 files changed
+313
-138
No files matched your search
+12
-12
@@ -36,15 +36,19 @@ pub async fn create_resource_access_token(
|
||||
state
|
||||
.check_rate_limit(client_limit_key, 50, window)
|
||||
.await
|
||||
.map_err(|seconds| ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
)))?;
|
||||
.map_err(|seconds| {
|
||||
ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
))
|
||||
})?;
|
||||
state
|
||||
.check_rate_limit(limit_key.clone(), 10, window)
|
||||
.await
|
||||
.map_err(|seconds| ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
)))?;
|
||||
.map_err(|seconds| {
|
||||
ApiError::rate_limited(&format!(
|
||||
"Too many password attempts. Try again in {seconds} seconds."
|
||||
))
|
||||
})?;
|
||||
match kind {
|
||||
"workspace" => {
|
||||
let workspace = db::find_workspace(&state.db, slug)
|
||||
@@ -81,12 +85,8 @@ pub async fn create_resource_access_token(
|
||||
.execute(state.db.pool())
|
||||
.await?;
|
||||
state.clear_rate_limit(&limit_key).await;
|
||||
let cookie = crate::security::resource_cookie(
|
||||
kind,
|
||||
slug,
|
||||
&token,
|
||||
state.anonymous_access_token_ttl_days,
|
||||
);
|
||||
let cookie =
|
||||
crate::security::resource_cookie(kind, slug, &token, state.anonymous_access_token_ttl_days);
|
||||
let mut response = Json(AccessTokenResponse {
|
||||
granted: true,
|
||||
expires_at,
|
||||
|
||||
@@ -13,6 +13,7 @@ use axum::{
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ApiError {
|
||||
status: StatusCode,
|
||||
message: String,
|
||||
|
||||
@@ -485,7 +485,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
add_favorite(
|
||||
let _ = add_favorite(
|
||||
State(state.clone()),
|
||||
headers.clone(),
|
||||
Json(FavoriteTarget {
|
||||
@@ -580,7 +580,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
add_favorite(
|
||||
let _ = add_favorite(
|
||||
State(state.clone()),
|
||||
headers.clone(),
|
||||
Json(FavoriteTarget {
|
||||
@@ -677,7 +677,7 @@ mod tests {
|
||||
HeaderValue::from_str(&format!("Bearer {share_token}")).unwrap(),
|
||||
);
|
||||
|
||||
add_favorite(
|
||||
let _ = add_favorite(
|
||||
State(state.clone()),
|
||||
headers.clone(),
|
||||
Json(FavoriteTarget {
|
||||
@@ -755,7 +755,7 @@ mod tests {
|
||||
HeaderValue::from_str(&format!("Bearer {access_token}")).unwrap(),
|
||||
);
|
||||
|
||||
add_favorite(
|
||||
let _ = add_favorite(
|
||||
State(state.clone()),
|
||||
headers.clone(),
|
||||
Json(FavoriteTarget {
|
||||
|
||||
+61
-49
@@ -101,10 +101,8 @@ pub async fn upload_pad_file(
|
||||
key = crate::storage::object_key("pads", pad.id, &file_token, &stored);
|
||||
}
|
||||
let stored_url = crate::file_urls::stored_file_path(&file_token, &stored);
|
||||
let public_url = crate::file_urls::public_file_url(
|
||||
state.files_public_url.as_deref(),
|
||||
&stored_url,
|
||||
);
|
||||
let public_url =
|
||||
crate::file_urls::public_file_url(state.files_public_url.as_deref(), &stored_url);
|
||||
let mime = mime_guess::from_path(&stored)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
@@ -123,7 +121,9 @@ pub async fn upload_pad_file(
|
||||
bytes.len() as i64,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": public_url, "mime_type": mime})))
|
||||
Ok(Json(
|
||||
serde_json::json!({"name": stored, "url": public_url, "mime_type": mime}),
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn content_references_file(content: &str, filename: &str, url: &str) -> bool {
|
||||
@@ -134,9 +134,7 @@ pub(super) fn content_references_file(content: &str, filename: &str, url: &str)
|
||||
let mut remaining = content;
|
||||
while let Some(index) = remaining.find(marker) {
|
||||
let after = &remaining[index + marker.len()..];
|
||||
let end = after
|
||||
.find(|character| character == ',' || character == ']')
|
||||
.unwrap_or(after.len());
|
||||
let end = after.find([',', ']']).unwrap_or(after.len());
|
||||
if after[..end].trim() == filename {
|
||||
return true;
|
||||
}
|
||||
@@ -202,10 +200,7 @@ pub async fn pad_files(
|
||||
};
|
||||
}
|
||||
file.created_at = db::normalize_timestamp(&file.created_at);
|
||||
file.url = crate::file_urls::public_file_url(
|
||||
state.files_public_url.as_deref(),
|
||||
&file.url,
|
||||
);
|
||||
file.url = crate::file_urls::public_file_url(state.files_public_url.as_deref(), &file.url);
|
||||
}
|
||||
Ok(Json(files))
|
||||
}
|
||||
@@ -225,14 +220,10 @@ pub async fn delete_pad_file(
|
||||
&headers,
|
||||
)
|
||||
.await?;
|
||||
let account_owner = crate::auth::is_resource_owner(
|
||||
&state,
|
||||
"pad",
|
||||
&pad.slug,
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
let account_owner =
|
||||
crate::auth::is_resource_owner(&state, "pad", &pad.slug, bearer_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", &pad.slug).await?;
|
||||
@@ -296,7 +287,12 @@ pub async fn upload_note_file(
|
||||
&workspace_slug,
|
||||
¬e_slug,
|
||||
password.as_deref(),
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
&headers,
|
||||
)
|
||||
@@ -312,7 +308,12 @@ pub async fn upload_note_file(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
resource_request_token(&headers, "workspace", &workspace_slug, access_token.as_deref()),
|
||||
resource_request_token(
|
||||
&headers,
|
||||
"workspace",
|
||||
&workspace_slug,
|
||||
access_token.as_deref(),
|
||||
),
|
||||
bearer_token(&headers),
|
||||
)
|
||||
.await?
|
||||
@@ -342,10 +343,8 @@ pub async fn upload_note_file(
|
||||
key = crate::storage::object_key("notes", note.id, &file_token, &stored);
|
||||
}
|
||||
let stored_url = crate::file_urls::stored_file_path(&file_token, &stored);
|
||||
let public_url = crate::file_urls::public_file_url(
|
||||
state.files_public_url.as_deref(),
|
||||
&stored_url,
|
||||
);
|
||||
let public_url =
|
||||
crate::file_urls::public_file_url(state.files_public_url.as_deref(), &stored_url);
|
||||
let mime = mime_guess::from_path(&stored)
|
||||
.first_or_octet_stream()
|
||||
.to_string();
|
||||
@@ -364,7 +363,9 @@ pub async fn upload_note_file(
|
||||
bytes.len() as i64,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(serde_json::json!({"name": stored, "url": public_url, "mime_type": mime})))
|
||||
Ok(Json(
|
||||
serde_json::json!({"name": stored, "url": public_url, "mime_type": mime}),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn delete_note(
|
||||
@@ -378,7 +379,12 @@ pub async fn delete_note(
|
||||
&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,
|
||||
)
|
||||
@@ -394,7 +400,12 @@ pub async fn delete_note(
|
||||
&headers,
|
||||
"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?
|
||||
@@ -415,12 +426,8 @@ pub async fn delete_note(
|
||||
.await
|
||||
.map_err(|_| ApiError::internal("Failed to delete note files"))?;
|
||||
}
|
||||
db::delete_resource_editor_state(
|
||||
&state.db,
|
||||
"note",
|
||||
&format!("{workspace_slug}/{note_slug}"),
|
||||
)
|
||||
.await?;
|
||||
db::delete_resource_editor_state(&state.db, "note", &format!("{workspace_slug}/{note_slug}"))
|
||||
.await?;
|
||||
db::delete_note(&state.db, note.id).await?;
|
||||
Ok(Json(serde_json::json!({"ok": true, "archived": false})))
|
||||
}
|
||||
@@ -436,7 +443,12 @@ pub async fn note_files(
|
||||
&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,
|
||||
)
|
||||
@@ -459,10 +471,7 @@ pub async fn note_files(
|
||||
};
|
||||
}
|
||||
file.created_at = db::normalize_timestamp(&file.created_at);
|
||||
file.url = crate::file_urls::public_file_url(
|
||||
state.files_public_url.as_deref(),
|
||||
&file.url,
|
||||
);
|
||||
file.url = crate::file_urls::public_file_url(state.files_public_url.as_deref(), &file.url);
|
||||
}
|
||||
Ok(Json(files))
|
||||
}
|
||||
@@ -478,7 +487,12 @@ pub async fn delete_note_file(
|
||||
&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,
|
||||
)
|
||||
@@ -627,10 +641,9 @@ async fn serve_token_file(
|
||||
HeaderValue::from_str(&format!("{disposition}; filename=\"{safe}\""))
|
||||
.expect("sanitized attachment filename"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::ACCEPT_RANGES,
|
||||
HeaderValue::from_static("bytes"),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
|
||||
response.headers_mut().insert(
|
||||
header::CONTENT_LENGTH,
|
||||
HeaderValue::from_str(&body_len.to_string()).expect("valid content length"),
|
||||
@@ -669,10 +682,9 @@ fn range_not_satisfiable(total_len: usize) -> Response {
|
||||
header::CONTENT_RANGE,
|
||||
HeaderValue::from_str(&format!("bytes */{total_len}")).expect("valid content range"),
|
||||
);
|
||||
response.headers_mut().insert(
|
||||
header::ACCEPT_RANGES,
|
||||
HeaderValue::from_static("bytes"),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert(header::ACCEPT_RANGES, HeaderValue::from_static("bytes"));
|
||||
response
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -583,6 +583,7 @@ async fn user_editor_preferences(
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn save_editor_settings(
|
||||
state: &SharedState,
|
||||
headers: &HeaderMap,
|
||||
@@ -1043,7 +1044,7 @@ pub async fn open_workspace(
|
||||
let page = query.page.max(1);
|
||||
let per_page = normalize_list_per_page(query.per_page);
|
||||
let total = notes.len();
|
||||
let total_pages = ((total + per_page - 1) / per_page).max(1);
|
||||
let total_pages = total.div_ceil(per_page).max(1);
|
||||
let page = page.min(total_pages);
|
||||
let start = (page - 1) * per_page;
|
||||
let notes = notes.into_iter().skip(start).take(per_page).collect();
|
||||
@@ -1142,7 +1143,7 @@ pub async fn create_note(
|
||||
title,
|
||||
payload.protect,
|
||||
created_by.as_deref(),
|
||||
created_by_guest_id.as_deref(),
|
||||
created_by_guest_id,
|
||||
)
|
||||
.await?;
|
||||
if let Some(content) = initial_content {
|
||||
|
||||
@@ -118,6 +118,7 @@ async fn share_session_redirect(
|
||||
response
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn render_editor_page(
|
||||
state: &SharedState,
|
||||
entrypoint: &str,
|
||||
|
||||
+1
-1
@@ -233,7 +233,7 @@ fn escape_filter(value: &str) -> String {
|
||||
b')' => result.push_str("\\29"),
|
||||
b'\\' => result.push_str("\\5c"),
|
||||
0 => result.push_str("\\00"),
|
||||
value if value < 0x20 || value >= 0x7f => result.push_str(&format!("\\{value:02x}")),
|
||||
value if !(0x20..0x7f).contains(&value) => result.push_str(&format!("\\{value:02x}")),
|
||||
value => result.push(value as char),
|
||||
}
|
||||
}
|
||||
|
||||
+13
-15
@@ -480,7 +480,7 @@ pub async fn register(
|
||||
.ok_or_else(|| AuthError::internal("Failed to create the account."))?;
|
||||
|
||||
let mut confirmation_token = None;
|
||||
if state.smtp.is_some() {
|
||||
if let Some(smtp) = &state.smtp {
|
||||
let token = random_confirmation_token();
|
||||
if state.account_confirmation_required {
|
||||
let expires = (Utc::now() + Duration::hours(24)).to_rfc3339();
|
||||
@@ -506,9 +506,7 @@ pub async fn register(
|
||||
tx.commit().await.map_err(AuthError::database)?;
|
||||
confirmation_token = Some(token.as_str());
|
||||
}
|
||||
if let Err(error) =
|
||||
send_registration_email(state.smtp.as_ref().unwrap(), &user, confirmation_token).await
|
||||
{
|
||||
if let Err(error) = send_registration_email(smtp, &user, confirmation_token).await {
|
||||
if state.account_confirmation_required {
|
||||
if let Err(delete_error) =
|
||||
sqlx::query(queries::get(state.db.kind(), queries::AUTH_DELETE_USER))
|
||||
@@ -638,16 +636,16 @@ pub async fn resend_confirmation(
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(AuthError::database)?;
|
||||
if let Some(value) = last_created {
|
||||
if let Ok(created) = chrono::DateTime::parse_from_rfc3339(&value) {
|
||||
let available = created.with_timezone(&Utc) + Duration::minutes(10);
|
||||
if available > Utc::now() {
|
||||
let seconds = (available - Utc::now()).num_seconds().max(1);
|
||||
return Err(AuthError::bad_request(&format!(
|
||||
"A new confirmation e-mail can be sent in {} minute(s).",
|
||||
(seconds + 59) / 60
|
||||
)));
|
||||
}
|
||||
if let Some(value) = last_created
|
||||
&& let Ok(created) = chrono::DateTime::parse_from_rfc3339(&value)
|
||||
{
|
||||
let available = created.with_timezone(&Utc) + Duration::minutes(10);
|
||||
if available > Utc::now() {
|
||||
let seconds = (available - Utc::now()).num_seconds().max(1);
|
||||
return Err(AuthError::bad_request(&format!(
|
||||
"A new confirmation e-mail can be sent in {} minute(s).",
|
||||
(seconds + 59) / 60
|
||||
)));
|
||||
}
|
||||
}
|
||||
sqlx::query(queries::get(
|
||||
@@ -1345,7 +1343,7 @@ pub async fn resources(
|
||||
let page = query.page.max(1);
|
||||
let per_page = normalized_per_page(query.per_page);
|
||||
let total = items.len();
|
||||
let total_pages = ((total + per_page - 1) / per_page).max(1);
|
||||
let total_pages = total.div_ceil(per_page).max(1);
|
||||
let page = page.min(total_pages);
|
||||
let start = (page - 1) * per_page;
|
||||
let items = items
|
||||
|
||||
+4
-5
@@ -219,11 +219,10 @@ impl Config {
|
||||
if self.static_dir.trim().is_empty() || self.files_dir.trim().is_empty() {
|
||||
return Err("STATIC_DIR and FILES_DIR cannot be empty".into());
|
||||
}
|
||||
if let Some(smtp) = &self.smtp {
|
||||
if !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://"))
|
||||
{
|
||||
return Err("PUBLIC_URL must start with http:// or https://".into());
|
||||
}
|
||||
if let Some(smtp) = &self.smtp
|
||||
&& !(smtp.public_url.starts_with("http://") || smtp.public_url.starts_with("https://"))
|
||||
{
|
||||
return Err("PUBLIC_URL must start with http:// or https://".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+13
-17
@@ -317,14 +317,11 @@ pub async fn note_slug_exists_any(
|
||||
workspace_id: i64,
|
||||
slug: &str,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let count: i64 = sqlx::query_scalar(queries::get(
|
||||
pool.kind(),
|
||||
queries::NOTE_SLUG_EXISTS_ANY,
|
||||
))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await?;
|
||||
let count: i64 = sqlx::query_scalar(queries::get(pool.kind(), queries::NOTE_SLUG_EXISTS_ANY))
|
||||
.bind(workspace_id)
|
||||
.bind(slug)
|
||||
.fetch_one(pool.pool())
|
||||
.await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
@@ -507,6 +504,7 @@ pub async fn save_revision(
|
||||
Ok((revision_id, updated_at))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn save_collaborative_revision(
|
||||
pool: &Database,
|
||||
note_id: i64,
|
||||
@@ -680,16 +678,14 @@ pub async fn create_pad(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn rename_pad(
|
||||
pool: &Database,
|
||||
pad_id: i64,
|
||||
title: &str,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
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 = ?",
|
||||
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)
|
||||
|
||||
@@ -447,10 +447,10 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow {
|
||||
pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
|
||||
let query = queries::get(pool.kind(), queries::PAD_PUBLIC_PAGE_DISABLED);
|
||||
if pool.kind() == DatabaseKind::Postgres {
|
||||
return Ok(sqlx::query_scalar::<_, bool>(query)
|
||||
return sqlx::query_scalar::<_, bool>(query)
|
||||
.bind(pad_id)
|
||||
.fetch_one(pool.pool())
|
||||
.await?);
|
||||
.await;
|
||||
}
|
||||
Ok(sqlx::query_scalar::<_, i64>(query)
|
||||
.bind(pad_id)
|
||||
@@ -462,10 +462,10 @@ pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bo
|
||||
pub async fn note_public_page_disabled(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
|
||||
let query = queries::get(pool.kind(), queries::NOTE_PUBLIC_PAGE_DISABLED);
|
||||
if pool.kind() == DatabaseKind::Postgres {
|
||||
return Ok(sqlx::query_scalar::<_, bool>(query)
|
||||
return sqlx::query_scalar::<_, bool>(query)
|
||||
.bind(note_id)
|
||||
.fetch_one(pool.pool())
|
||||
.await?);
|
||||
.await;
|
||||
}
|
||||
Ok(sqlx::query_scalar::<_, i64>(query)
|
||||
.bind(note_id)
|
||||
|
||||
+1
-4
@@ -53,10 +53,7 @@ pub fn canonical_file_path(value: &str) -> Option<String> {
|
||||
let path_start = after_scheme.find('/')?;
|
||||
&after_scheme[path_start..]
|
||||
};
|
||||
let path = path
|
||||
.split(|character| matches!(character, '?' | '#'))
|
||||
.next()
|
||||
.unwrap_or(path);
|
||||
let path = path.split(['?', '#']).next().unwrap_or(path);
|
||||
if !path.starts_with(FILE_ROUTE_PREFIX) {
|
||||
return None;
|
||||
}
|
||||
|
||||
+2
-3
@@ -86,10 +86,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.database_url
|
||||
.strip_prefix("sqlite://")
|
||||
.and_then(|v| v.split('?').next())
|
||||
&& let Some(parent) = std::path::Path::new(path).parent()
|
||||
{
|
||||
if let Some(parent) = std::path::Path::new(path).parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
info!("connecting to database");
|
||||
let db = Database::connect(&config.database_url, config.database_max_connections).await?;
|
||||
|
||||
+4
-1
@@ -203,7 +203,10 @@ pub fn websocket_origin_allowed(headers: &HeaderMap) -> bool {
|
||||
|
||||
fn resource_cookie_name(kind: &str, slug: &str) -> String {
|
||||
let digest = Sha256::digest(format!("{kind}:{slug}").as_bytes());
|
||||
format!("{RESOURCE_ACCESS_COOKIE_PREFIX}{}", hex::encode(&digest[..12]))
|
||||
format!(
|
||||
"{RESOURCE_ACCESS_COOKIE_PREFIX}{}",
|
||||
hex::encode(&digest[..12])
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_resource_access_cookie_name(name: &str) -> bool {
|
||||
|
||||
+6
-10
@@ -129,6 +129,7 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
db: Database,
|
||||
asset_version: String,
|
||||
@@ -183,14 +184,13 @@ impl AppState {
|
||||
let mut limits = self.rate_limits.lock().await;
|
||||
if !limits.contains_key(&key) && limits.len() >= MAX_RATE_LIMIT_ENTRIES {
|
||||
limits.retain(|_, entry| now.duration_since(entry.started_at) < entry.window);
|
||||
if limits.len() >= MAX_RATE_LIMIT_ENTRIES {
|
||||
if let Some(oldest_key) = limits
|
||||
if limits.len() >= MAX_RATE_LIMIT_ENTRIES
|
||||
&& let Some(oldest_key) = limits
|
||||
.iter()
|
||||
.min_by_key(|(_, entry)| entry.started_at)
|
||||
.map(|(key, _)| key.clone())
|
||||
{
|
||||
limits.remove(&oldest_key);
|
||||
}
|
||||
{
|
||||
limits.remove(&oldest_key);
|
||||
}
|
||||
}
|
||||
let entry = limits.entry(key).or_insert(RateLimitEntry {
|
||||
@@ -283,11 +283,7 @@ impl AppState {
|
||||
pub async fn pad_channel(&self, slug: &str) -> broadcast::Sender<RoomEvent> {
|
||||
self.channel_for_key(Self::pad_room_key(slug)).await
|
||||
}
|
||||
pub async fn notify_pad_password_required(
|
||||
&self,
|
||||
slug: &str,
|
||||
except_client_id: Option<String>,
|
||||
) {
|
||||
pub async fn notify_pad_password_required(&self, slug: &str, except_client_id: Option<String>) {
|
||||
let key = Self::pad_room_key(slug);
|
||||
let sender = self.channels.read().await.get(&key).cloned();
|
||||
if let Some(sender) = sender {
|
||||
|
||||
+12
-3
@@ -72,9 +72,18 @@ fn video_alias_is_still_attached() {
|
||||
#[test]
|
||||
fn byte_ranges_support_video_seeking() {
|
||||
assert_eq!(parse_byte_range("bytes=0-99", 1_000), Ok(Some((0, 100))));
|
||||
assert_eq!(parse_byte_range("bytes=500-", 1_000), Ok(Some((500, 1_000))));
|
||||
assert_eq!(parse_byte_range("bytes=-100", 1_000), Ok(Some((900, 1_000))));
|
||||
assert_eq!(parse_byte_range("bytes=900-2000", 1_000), Ok(Some((900, 1_000))));
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=500-", 1_000),
|
||||
Ok(Some((500, 1_000)))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=-100", 1_000),
|
||||
Ok(Some((900, 1_000)))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_byte_range("bytes=900-2000", 1_000),
|
||||
Ok(Some((900, 1_000)))
|
||||
);
|
||||
assert_eq!(parse_byte_range("bytes=1000-", 1_000), Err(()));
|
||||
assert_eq!(parse_byte_range("bytes=0-1,4-5", 1_000), Err(()));
|
||||
}
|
||||
@@ -14,8 +14,8 @@ fn boolean_projections_are_normalized_for_sqlx_any() {
|
||||
// MySQL BOOLEAN is TINYINT(1), which sqlx::Any 0.8 cannot map directly.
|
||||
for (query, expected_casts) in [
|
||||
(Query::RESOURCE_EDITOR_SETTINGS_SELECT, 1),
|
||||
(Query::EDITOR_PREFERENCES_SELECT_PAD, 5),
|
||||
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 5),
|
||||
(Query::EDITOR_PREFERENCES_SELECT_PAD, 6),
|
||||
(Query::EDITOR_PREFERENCES_SELECT_NOTE, 6),
|
||||
(Query::AUTH_USER_BY_EXTERNAL_ID, 1),
|
||||
(Query::AUTH_USER_BY_SESSION, 1),
|
||||
(Query::AUTH_USER_BY_NICKNAME, 1),
|
||||
@@ -27,7 +27,7 @@ fn boolean_projections_are_normalized_for_sqlx_any() {
|
||||
(Query::USER_FAVORITE_NOTE_LIST, 2),
|
||||
(Query::PAD_PUBLIC_PAGE_DISABLED, 1),
|
||||
(Query::NOTE_PUBLIC_PAGE_DISABLED, 1),
|
||||
(Query::Q001, 1),
|
||||
(Query::Q001, 2),
|
||||
(Query::Q003, 1),
|
||||
(Query::Q004, 1),
|
||||
(Query::Q011, 1),
|
||||
|
||||
@@ -84,7 +84,8 @@ fn rejects_missing_websocket_origin() {
|
||||
|
||||
#[test]
|
||||
fn secure_cookies_are_not_script_readable() {
|
||||
let value = session_cookie("abc123", 7).to_str().unwrap();
|
||||
let cookie = session_cookie("abc123", 7);
|
||||
let value = cookie.to_str().unwrap();
|
||||
assert!(value.contains("HttpOnly"));
|
||||
assert!(value.contains("Secure"));
|
||||
assert!(value.contains("SameSite=Lax"));
|
||||
@@ -93,9 +94,8 @@ fn secure_cookies_are_not_script_readable() {
|
||||
|
||||
#[test]
|
||||
fn share_sessions_use_separate_scoped_opaque_cookies() {
|
||||
let value = share_session_cookie("workspace", "private-space", "opaque", 600)
|
||||
.to_str()
|
||||
.unwrap();
|
||||
let cookie = share_session_cookie("workspace", "private-space", "opaque", 600);
|
||||
let value = cookie.to_str().unwrap();
|
||||
assert!(value.starts_with("__Host-rustpad_share_"));
|
||||
assert!(value.contains("=opaque;"));
|
||||
assert!(value.contains("Max-Age=600"));
|
||||
@@ -166,7 +166,8 @@ fn csrf_requires_matching_cookie_and_header() {
|
||||
#[test]
|
||||
fn csrf_cookie_is_strict_and_script_readable() {
|
||||
let token = "a".repeat(CSRF_TOKEN_BYTES * 2);
|
||||
let value = csrf_cookie(&token).to_str().unwrap();
|
||||
let cookie = csrf_cookie(&token);
|
||||
let value = cookie.to_str().unwrap();
|
||||
assert!(value.contains("Secure"));
|
||||
assert!(value.contains("SameSite=Strict"));
|
||||
assert!(!value.contains("HttpOnly"));
|
||||
|
||||
@@ -594,6 +594,7 @@ pub async fn upgrade(
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_socket(
|
||||
mut socket: WebSocket,
|
||||
state: SharedState,
|
||||
|
||||
@@ -77,6 +77,8 @@ pub async fn upgrade_pad(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn handle_pad_socket(
|
||||
mut socket: WebSocket,
|
||||
state: SharedState,
|
||||
|
||||
Reference in new issue
Block a user