new functions

This commit is contained in:
Mateusz Gruszczyński
2026-07-21 23:58:01 +02:00
parent fd2641780b
commit fa8a0d687f
18 changed files with 478 additions and 79 deletions
+43 -2
View File
@@ -27,6 +27,7 @@ pub struct PublicPageResponse {
title: String,
content: String,
updated_at: String,
allow_task_updates: bool,
}
#[derive(Debug, Deserialize)]
@@ -48,6 +49,20 @@ pub struct PasswordRequest {
password: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct PublishRequest {
#[serde(default)]
password: Option<String>,
#[serde(default)]
allow_task_updates: bool,
}
#[derive(Debug, Deserialize)]
pub struct PublicTaskUpdateRequest {
source_line: usize,
checked: bool,
}
#[derive(Debug, Deserialize)]
pub struct CreateNoteRequest {
name: String,
@@ -100,6 +115,7 @@ pub struct NoteInfo {
title: String,
protected: bool,
note_protected: bool,
allow_public_task_updates: bool,
created_at: String,
updated_at: String,
}
@@ -206,6 +222,7 @@ pub async fn note_info(
title: note.title,
protected: workspace.password_hash.is_some(),
note_protected: note.protected,
allow_public_task_updates: db::note_public_task_updates(&state.db, note.id).await?,
created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at),
}))
@@ -374,6 +391,7 @@ pub struct PadInfo {
slug: String,
title: String,
protected: bool,
allow_public_task_updates: bool,
created_at: String,
updated_at: String,
}
@@ -410,6 +428,7 @@ pub async fn pad_info(
slug: pad.slug,
title: pad.title,
protected: pad.password_hash.is_some(),
allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?,
created_at: db::normalize_timestamp(&pad.created_at),
updated_at: db::normalize_timestamp(&pad.updated_at),
}))
@@ -418,20 +437,22 @@ pub async fn pad_info(
pub async fn publish_pad_page(
State(state): State<SharedState>,
Path(slug): Path<String>,
Json(payload): Json<PasswordRequest>,
Json(payload): Json<PublishRequest>,
) -> Result<Json<PublishResponse>, ApiError> {
let pad = authorized_pad(&state, &slug, payload.password.as_deref()).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?;
Ok(Json(PublishResponse { url: format!("/s/{token}") }))
}
pub async fn publish_note_page(
State(state): State<SharedState>,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<PasswordRequest>,
Json(payload): Json<PublishRequest>,
) -> Result<Json<PublishResponse>, ApiError> {
let (_, note) = authorized_note(&state, &workspace_slug, &note_slug, payload.password.as_deref()).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?;
Ok(Json(PublishResponse { url: format!("/s/{token}") }))
}
@@ -446,6 +467,23 @@ pub async fn public_page(
title: page.title,
content: page.content,
updated_at: db::normalize_timestamp(&page.updated_at),
allow_task_updates: page.allow_task_updates,
}))
}
pub async fn update_public_task(
State(state): State<SharedState>,
Path(token): Path<String>,
Json(payload): Json<PublicTaskUpdateRequest>,
) -> Result<Json<PublicPageResponse>, ApiError> {
let current = db::find_published_page(&state.db, &token).await?.ok_or_else(ApiError::not_found_note)?;
if !current.allow_task_updates { return Err(ApiError::forbidden("Task updates are disabled for this page")); }
let page = db::update_public_task(&state.db, &token, payload.source_line, payload.checked).await?.ok_or_else(ApiError::not_found_note)?;
Ok(Json(PublicPageResponse {
title: page.title,
content: page.content,
updated_at: db::normalize_timestamp(&page.updated_at),
allow_task_updates: page.allow_task_updates,
}))
}
@@ -767,6 +805,9 @@ impl ApiError {
message: "Invalid password".into(),
}
}
fn forbidden(message: &str) -> Self {
Self { status: StatusCode::FORBIDDEN, message: message.into() }
}
fn not_found_workspace() -> Self {
Self {
status: StatusCode::NOT_FOUND,
+1
View File
@@ -20,6 +20,7 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
.route("/f/{token}/{filename}", get(api::download_file))
.route("/files/{directory}/{filename}", get(api::download_legacy_file))
.route("/api/public/{token}", get(api::public_page))
.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}/history", post(api::pad_history))
+86 -5
View File
@@ -382,14 +382,42 @@ pub async fn list_pad_revisions(
.await
}
#[derive(Debug, Clone, Serialize, FromRow)]
#[derive(Debug, Clone, Serialize)]
pub struct PublishedPage {
pub token: String,
pub pad_id: Option<i64>,
pub note_id: Option<i64>,
pub allow_task_updates: bool,
pub title: String,
pub content: String,
pub updated_at: String,
}
#[derive(Debug, Clone, FromRow)]
struct PublishedPageRow {
token: String,
pad_id: Option<i64>,
note_id: Option<i64>,
allow_task_updates: i64,
title: String,
content: String,
updated_at: String,
}
impl From<PublishedPageRow> for PublishedPage {
fn from(value: PublishedPageRow) -> Self {
Self {
token: value.token,
pad_id: value.pad_id,
note_id: value.note_id,
allow_task_updates: value.allow_task_updates != 0,
title: value.title,
content: value.content,
updated_at: value.updated_at,
}
}
}
pub async fn publish_pad(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
if let Some(token) = sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q017))
.bind(pad_id)
@@ -425,10 +453,63 @@ pub async fn publish_note(pool: &Database, note_id: i64) -> Result<String, sqlx:
}
pub async fn find_published_page(pool: &Database, token: &str) -> Result<Option<PublishedPage>, sqlx::Error> {
sqlx::query_as::<_, PublishedPage>(queries::get(pool.kind(), queries::Q021))
.bind(token)
.fetch_optional(pool.pool())
.await
Ok(sqlx::query_as::<_, PublishedPageRow>(queries::get(pool.kind(), queries::Q021))
.bind(token)
.fetch_optional(pool.pool())
.await?
.map(Into::into))
}
pub async fn pad_public_task_updates(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q044))
.bind(pad_id)
.fetch_optional(pool.pool())
.await?
.unwrap_or(0);
Ok(value != 0)
}
pub async fn note_public_task_updates(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
let value = sqlx::query_scalar::<_, i64>(queries::get(pool.kind(), queries::Q045))
.bind(note_id)
.fetch_optional(pool.pool())
.await?
.unwrap_or(0);
Ok(value != 0)
}
pub async fn set_pad_public_task_updates(pool: &Database, pad_id: i64, allow: bool) -> Result<(), sqlx::Error> {
publish_pad(pool, pad_id).await?;
sqlx::query(queries::get(pool.kind(), queries::Q040)).bind(if allow { 1i64 } else { 0i64 }).bind(pad_id).execute(pool.pool()).await?;
Ok(())
}
pub async fn set_note_public_task_updates(pool: &Database, note_id: i64, allow: bool) -> Result<(), sqlx::Error> {
publish_note(pool, note_id).await?;
sqlx::query(queries::get(pool.kind(), queries::Q041)).bind(if allow { 1i64 } else { 0i64 }).bind(note_id).execute(pool.pool()).await?;
Ok(())
}
pub async fn update_public_task(pool: &Database, token: &str, source_line: usize, checked: bool) -> Result<Option<PublishedPage>, sqlx::Error> {
let Some(mut page) = find_published_page(pool, token).await? else { return Ok(None); };
if !page.allow_task_updates || source_line == 0 { return Ok(Some(page)); }
let mut lines: Vec<String> = page.content.split('\n').map(str::to_owned).collect();
let Some(line) = lines.get_mut(source_line - 1) else { return Ok(Some(page)); };
let bytes = line.as_bytes();
let mut i = 0usize;
while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; }
if i >= bytes.len() || !matches!(bytes[i], b'-' | b'*' | b'+') { return Ok(Some(page)); }
i += 1;
while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; }
if i + 2 >= bytes.len() || bytes[i] != b'[' || !matches!(bytes[i + 1], b' ' | b'x' | b'X') || bytes[i + 2] != b']' { return Ok(Some(page)); }
line.replace_range(i + 1..i + 2, if checked { "x" } else { " " });
page.content = lines.join("\n");
if let Some(id) = page.pad_id {
sqlx::query(queries::get(pool.kind(), queries::Q042)).bind(&page.content).bind(id).execute(pool.pool()).await?;
} else if let Some(id) = page.note_id {
sqlx::query(queries::get(pool.kind(), queries::Q043)).bind(&page.content).bind(id).execute(pool.pool()).await?;
}
find_published_page(pool, token).await
}
pub async fn pad_file_token(pool: &Database, pad_id: i64) -> Result<String, sqlx::Error> {
+9 -1
View File
@@ -21,7 +21,7 @@ pub const Q017: &str = "SELECT token FROM published_pages WHERE pad_id = ?";
pub const Q018: &str = "INSERT INTO published_pages (token, pad_id) VALUES (?, ?)";
pub const Q019: &str = "SELECT token FROM published_pages WHERE note_id = ?";
pub const Q020: &str = "INSERT INTO published_pages (token, note_id) VALUES (?, ?)";
pub const Q021: &str = "SELECT pp.token, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?";
pub const Q021: &str = "SELECT pp.token, pp.pad_id, pp.note_id, CASE WHEN pp.allow_task_updates THEN 1 ELSE 0 END AS allow_task_updates, COALESCE(p.title, n.title) AS title, COALESCE(p.content, n.content) AS content, COALESCE(p.updated_at, n.updated_at) AS updated_at FROM published_pages pp LEFT JOIN pads p ON p.id = pp.pad_id LEFT JOIN notes n ON n.id = pp.note_id WHERE pp.token = ?";
pub const Q022: &str = "SELECT file_token FROM pads WHERE id = ?";
pub const Q023: &str = "UPDATE pads SET file_token = ? WHERE id = ? AND file_token IS NULL";
pub const Q024: &str = "SELECT file_token FROM notes WHERE id = ?";
@@ -65,3 +65,11 @@ pub fn get(kind: DatabaseKind, query: &'static str) -> &'static str {
cache.insert(cache_key, converted);
converted
}
pub const Q040: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE pad_id = ?";
pub const Q041: &str = "UPDATE published_pages SET allow_task_updates = ? WHERE note_id = ?";
pub const Q042: &str = "UPDATE pads SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
pub const Q043: &str = "UPDATE notes SET content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?";
pub const Q044: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE pad_id = ?";
pub const Q045: &str = "SELECT CASE WHEN allow_task_updates THEN 1 ELSE 0 END FROM published_pages WHERE note_id = ?";