license and some functions

This commit is contained in:
Mateusz Gruszczyński
2026-07-29 14:03:46 +02:00
parent 0655cfe48c
commit ef8dfc67c1
56 changed files with 2586 additions and 547 deletions
+9 -1
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[derive(Debug, Deserialize)]
@@ -92,4 +101,3 @@ pub async fn verify_resource_access_token(
pub(super) fn hash_access_token(token: &str) -> String {
hex::encode(Sha256::digest(token.as_bytes()))
}
+9
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use axum::{
Json,
http::StatusCode,
+10 -2
View File
@@ -1,5 +1,14 @@
use super::*;
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::pads_public::authorized_pad;
use super::*;
pub async fn upload_pad_file(
State(state): State<SharedState>,
@@ -485,4 +494,3 @@ fn sanitize_filename(value: &str) -> String {
clean.chars().take(160).collect()
}
}
+96 -18
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
mod access_tokens;
mod error;
mod files;
@@ -178,7 +187,11 @@ pub struct EditorSettingsRequest {
colors_enabled: bool,
}
async fn editor_settings(state: &SharedState, kind: &str, slug: &str) -> Result<(String, bool), ApiError> {
async fn editor_settings(
state: &SharedState,
kind: &str,
slug: &str,
) -> Result<(String, bool), ApiError> {
let row: Option<(String, i64)> = sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_EDITOR_SETTINGS_SELECT,
@@ -187,27 +200,70 @@ async fn editor_settings(state: &SharedState, kind: &str, slug: &str) -> Result<
.bind(slug)
.fetch_optional(state.db.pool())
.await?;
Ok(row.map(|(mode, colors)| (if mode == "full" { "full".into() } else { "simple".into() }, colors != 0))
Ok(row
.map(|(mode, colors)| {
(
if mode == "full" {
"full".into()
} else {
"simple".into()
},
colors != 0,
)
})
.unwrap_or_else(|| ("simple".into(), true)))
}
async fn save_editor_settings(
state: &SharedState, headers: &HeaderMap, permission_kind: &str, permission_slug: &str,
settings_kind: &str, settings_slug: &str, payload: EditorSettingsRequest,
state: &SharedState,
headers: &HeaderMap,
permission_kind: &str,
permission_slug: &str,
settings_kind: &str,
settings_slug: &str,
payload: EditorSettingsRequest,
) -> Result<Json<serde_json::Value>, ApiError> {
let permission = crate::auth::resource_permission(state, permission_kind, permission_slug, bearer_token(headers))
.await.map_err(|e| ApiError::forbidden(&e.message))?;
let permission = crate::auth::resource_permission(
state,
permission_kind,
permission_slug,
bearer_token(headers),
)
.await
.map_err(|e| ApiError::forbidden(&e.message))?;
if permission.as_deref() != Some("rw") {
return Err(ApiError::forbidden("Read and write access is required to save editor settings"));
return Err(ApiError::forbidden(
"Read and write access is required to save editor settings",
));
}
let mode = match payload.authorship_mode.as_str() { "simple" => "simple", "full" | "advanced" => "full", _ => return Err(ApiError::bad_request("Invalid authorship mode")) };
let mode = match payload.authorship_mode.as_str() {
"simple" => "simple",
"full" | "advanced" => "full",
_ => return Err(ApiError::bad_request("Invalid authorship mode")),
};
let mut tx = state.db.pool().begin().await?;
sqlx::query(queries::get(state.db.kind(), queries::RESOURCE_EDITOR_SETTINGS_DELETE))
.bind(settings_kind).bind(settings_slug).execute(&mut *tx).await?;
sqlx::query(queries::get(state.db.kind(), queries::RESOURCE_EDITOR_SETTINGS_INSERT))
.bind(settings_kind).bind(settings_slug).bind(mode).bind(payload.colors_enabled).execute(&mut *tx).await?;
sqlx::query(queries::get(
state.db.kind(),
queries::RESOURCE_EDITOR_SETTINGS_DELETE,
))
.bind(settings_kind)
.bind(settings_slug)
.execute(&mut *tx)
.await?;
sqlx::query(queries::get(
state.db.kind(),
queries::RESOURCE_EDITOR_SETTINGS_INSERT,
))
.bind(settings_kind)
.bind(settings_slug)
.bind(mode)
.bind(payload.colors_enabled)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(Json(serde_json::json!({"authorship_mode": mode, "colors_enabled": payload.colors_enabled})))
Ok(Json(
serde_json::json!({"authorship_mode": mode, "colors_enabled": payload.colors_enabled}),
))
}
pub async fn create_workspace(
@@ -485,9 +541,22 @@ pub async fn note_info(
let color_slug = format!("{}/{}", workspace_slug, note_slug);
let (global_color, note_color) = editor_colors(&state, &headers, "note", &color_slug).await?;
let (authorship_mode, colors_enabled) = editor_settings(&state, "note", &color_slug).await?;
let can_save_editor_settings = crate::auth::resource_permission(&state, "workspace", &workspace_slug, bearer_token(&headers)).await.ok().flatten().as_deref() == Some("rw");
let can_save_editor_settings = crate::auth::resource_permission(
&state,
"workspace",
&workspace_slug,
bearer_token(&headers),
)
.await
.ok()
.flatten()
.as_deref()
== Some("rw");
if workspace.is_private == 0 && !db::note_public_page_disabled(&state.db, note.id).await? && !db::note_public_page_enabled(&state.db, note.id).await? {
if workspace.is_private == 0
&& !db::note_public_page_disabled(&state.db, note.id).await?
&& !db::note_public_page_enabled(&state.db, note.id).await?
{
db::publish_note(&state.db, note.id).await?;
}
Ok(Json(NoteInfo {
@@ -533,12 +602,22 @@ pub async fn note_info(
}
pub async fn set_note_editor_settings(
State(state): State<SharedState>, headers: HeaderMap,
State(state): State<SharedState>,
headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<EditorSettingsRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let settings_slug = format!("{}/{}", workspace_slug, note_slug);
save_editor_settings(&state, &headers, "workspace", &workspace_slug, "note", &settings_slug, payload).await
save_editor_settings(
&state,
&headers,
"workspace",
&workspace_slug,
"note",
&settings_slug,
payload,
)
.await
}
pub async fn history(
@@ -840,4 +919,3 @@ async fn unique_note_slug(
}
Err(ApiError::internal("Failed to create a unique address"))
}
+99 -27
View File
@@ -1,3 +1,12 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
use super::*;
#[derive(Debug, Deserialize)]
@@ -84,8 +93,17 @@ pub async fn pad_info(
.await?;
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
let (authorship_mode, colors_enabled) = editor_settings(&state, "pad", &slug).await?;
let can_save_editor_settings = crate::auth::resource_permission(&state, "pad", &slug, bearer_token(&headers)).await.ok().flatten().as_deref() == Some("rw");
if pad.is_private == 0 && !db::pad_public_page_disabled(&state.db, pad.id).await? && !db::pad_public_page_enabled(&state.db, pad.id).await? {
let can_save_editor_settings =
crate::auth::resource_permission(&state, "pad", &slug, bearer_token(&headers))
.await
.ok()
.flatten()
.as_deref()
== Some("rw");
if pad.is_private == 0
&& !db::pad_public_page_disabled(&state.db, pad.id).await?
&& !db::pad_public_page_enabled(&state.db, pad.id).await?
{
db::publish_pad(&state.db, pad.id).await?;
}
Ok(Json(PadInfo {
@@ -115,7 +133,9 @@ pub async fn pad_info(
}
pub async fn set_pad_editor_settings(
State(state): State<SharedState>, headers: HeaderMap, Path(slug): Path<String>,
State(state): State<SharedState>,
headers: HeaderMap,
Path(slug): Path<String>,
Json(payload): Json<EditorSettingsRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
save_editor_settings(&state, &headers, "pad", &slug, "pad", &slug, payload).await
@@ -207,14 +227,18 @@ pub async fn publish_pad_page(
if !enabled {
db::unpublish_pad(&state.db, pad.id).await?;
db::set_pad_public_page_disabled(&state.db, pad.id, true).await?;
return Ok(Json(PublishResponse { url: None, enabled: false }));
return Ok(Json(PublishResponse {
url: None,
enabled: false,
}));
}
db::set_pad_public_page_disabled(&state.db, pad.id, false).await?;
let token = db::publish_pad(&state.db, pad.id).await?;
db::set_pad_public_task_updates(&state.db, pad.id, payload.allow_task_updates).await?;
db::set_pad_public_page_unprotected(&state.db, pad.id, payload.unprotect_page).await?;
Ok(Json(PublishResponse {
url: Some(format!("/s/{token}")), enabled: true,
url: Some(format!("/s/{token}")),
enabled: true,
}))
}
@@ -252,50 +276,99 @@ pub async fn publish_note_page(
if !enabled {
db::unpublish_note(&state.db, note.id).await?;
db::set_note_public_page_disabled(&state.db, note.id, true).await?;
return Ok(Json(PublishResponse { url: None, enabled: false }));
return Ok(Json(PublishResponse {
url: None,
enabled: false,
}));
}
db::set_note_public_page_disabled(&state.db, note.id, false).await?;
let token = db::publish_note(&state.db, note.id).await?;
db::set_note_public_task_updates(&state.db, note.id, payload.allow_task_updates).await?;
db::set_note_public_page_unprotected(&state.db, note.id, payload.unprotect_page).await?;
Ok(Json(PublishResponse {
url: Some(format!("/s/{token}")), enabled: true,
url: Some(format!("/s/{token}")),
enabled: true,
}))
}
fn page_password(headers: &HeaderMap) -> Option<&str> {
headers.get("x-rustpad-page-password").and_then(|value| value.to_str().ok()).map(str::trim).filter(|value| !value.is_empty())
headers
.get("x-rustpad-page-password")
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
}
async fn ensure_public_page_access(state: &SharedState, headers: &HeaderMap, page: &db::PublishedPage) -> Result<(), ApiError> {
async fn ensure_public_page_access(
state: &SharedState,
headers: &HeaderMap,
page: &db::PublishedPage,
) -> Result<(), ApiError> {
let password = page_password(headers);
let bearer = bearer_token(headers);
if let Some(pad_id) = page.pad_id {
if db::pad_public_page_unprotected(&state.db, pad_id).await? { return Ok(()); }
if db::pad_public_page_unprotected(&state.db, pad_id).await? {
return Ok(());
}
let sql = match state.db.kind() {
crate::database::DatabaseKind::Postgres => "SELECT slug FROM pads WHERE id = $1",
_ => "SELECT slug FROM pads WHERE id = ?",
};
let slug: Option<String> = sqlx::query_scalar(sql).bind(pad_id).fetch_optional(state.db.pool()).await?;
let Some(slug) = slug else { return Err(ApiError::not_found_note()); };
let pad = db::find_pad(&state.db, &slug).await?.ok_or_else(ApiError::not_found_note)?;
if db::verify_pad_password(&pad, password) { return Ok(()); }
if verify_resource_access_token(state, "pad", &slug, bearer).await? { return Ok(()); }
return if pad.password_hash.is_some() { Err(ApiError::forbidden("Password required or incorrect.")) } else { Err(ApiError::forbidden("This published page is protected.")) };
let slug: Option<String> = sqlx::query_scalar(sql)
.bind(pad_id)
.fetch_optional(state.db.pool())
.await?;
let Some(slug) = slug else {
return Err(ApiError::not_found_note());
};
let pad = db::find_pad(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_note)?;
if db::verify_pad_password(&pad, password) {
return Ok(());
}
if verify_resource_access_token(state, "pad", &slug, bearer).await? {
return Ok(());
}
return if pad.password_hash.is_some() {
Err(ApiError::forbidden("Password required or incorrect."))
} else {
Err(ApiError::forbidden("This published page is protected."))
};
}
if let Some(note_id) = page.note_id {
if db::note_public_page_unprotected(&state.db, note_id).await? { return Ok(()); }
if db::note_public_page_unprotected(&state.db, note_id).await? {
return Ok(());
}
let sql = match state.db.kind() {
crate::database::DatabaseKind::Postgres => "SELECT w.slug FROM notes n JOIN workspaces w ON w.id = n.workspace_id WHERE n.id = $1",
_ => "SELECT w.slug FROM notes n JOIN workspaces w ON w.id = n.workspace_id WHERE n.id = ?",
crate::database::DatabaseKind::Postgres => {
"SELECT w.slug FROM notes n JOIN workspaces w ON w.id = n.workspace_id WHERE n.id = $1"
}
_ => {
"SELECT w.slug FROM notes n JOIN workspaces w ON w.id = n.workspace_id WHERE n.id = ?"
}
};
let slug: Option<String> = sqlx::query_scalar(sql)
.bind(note_id)
.fetch_optional(state.db.pool())
.await?;
let Some(slug) = slug else {
return Err(ApiError::not_found_note());
};
let workspace = db::find_workspace(&state.db, &slug)
.await?
.ok_or_else(ApiError::not_found_workspace)?;
if db::verify_workspace_password(&workspace, password) {
return Ok(());
}
if verify_resource_access_token(state, "workspace", &slug, bearer).await? {
return Ok(());
}
return if workspace.password_hash.is_some() {
Err(ApiError::forbidden("Password required or incorrect."))
} else {
Err(ApiError::forbidden("This published page is protected."))
};
let slug: Option<String> = sqlx::query_scalar(sql).bind(note_id).fetch_optional(state.db.pool()).await?;
let Some(slug) = slug else { return Err(ApiError::not_found_note()); };
let workspace = db::find_workspace(&state.db, &slug).await?.ok_or_else(ApiError::not_found_workspace)?;
if db::verify_workspace_password(&workspace, password) { return Ok(()); }
if verify_resource_access_token(state, "workspace", &slug, bearer).await? { return Ok(()); }
return if workspace.password_hash.is_some() { Err(ApiError::forbidden("Password required or incorrect.")) } else { Err(ApiError::forbidden("This published page is protected.")) };
}
Err(ApiError::not_found_note())
}
@@ -461,4 +534,3 @@ async fn unique_pad_slug(state: &SharedState, base: &str) -> Result<String, ApiE
}
Err(ApiError::internal("Failed to create a unique address"))
}