feat: hide entry content from list, add GET /api/entries/:id/content endpoint

Content is no longer returned in the entries list. A separate endpoint
returns plain text content for clipboard copying on demand.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-03-17 16:23:05 +03:00
parent cc0bdedd88
commit 37f86740a2
27 changed files with 5700 additions and 0 deletions

1
backend/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

2065
backend/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

13
backend/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "backend"
version = "0.1.0"
edition = "2024"
[dependencies]
axum = "0.8"
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "chrono"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower-http = { version = "0.6", features = ["cors"] }
chrono = { version = "0.4", features = ["serde"] }

14
backend/src/db.rs Normal file
View File

@@ -0,0 +1,14 @@
use sqlx::PgPool;
pub async fn init_db(pool: &PgPool) {
sqlx::query(
"CREATE TABLE IF NOT EXISTS entries (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
content TEXT NOT NULL
)",
)
.execute(pool)
.await
.expect("Failed to create entries table");
}

34
backend/src/main.rs Normal file
View File

@@ -0,0 +1,34 @@
mod db;
mod routes;
use axum::{Router, routing::{get, delete}};
use sqlx::postgres::PgPoolOptions;
use tower_http::cors::CorsLayer;
#[tokio::main]
async fn main() {
let database_url =
std::env::var("DATABASE_URL").unwrap_or_else(|_| "postgres://bbb:bbb@localhost:5432/bbb".to_string());
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to connect to database");
db::init_db(&pool).await;
let app = Router::new()
.route("/api/entries", get(routes::get_entries).post(routes::create_entry))
.route("/api/entries/{id}", delete(routes::delete_entry))
.route("/api/entries/{id}/content", get(routes::get_entry_content))
.layer(CorsLayer::permissive())
.with_state(pool);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("Failed to bind to port 3000");
println!("Server running on http://localhost:3000");
axum::serve(listener, app).await.expect("Server error");
}

80
backend/src/routes.rs Normal file
View File

@@ -0,0 +1,80 @@
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
#[derive(Serialize, sqlx::FromRow)]
pub struct Entry {
pub id: i32,
pub created_at: Option<DateTime<Utc>>,
pub content: String,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct EntryMeta {
pub id: i32,
pub created_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize)]
pub struct CreateEntry {
pub content: String,
}
pub async fn get_entries(State(pool): State<PgPool>) -> Result<Json<Vec<EntryMeta>>, StatusCode> {
let entries = sqlx::query_as::<_, EntryMeta>("SELECT id, created_at FROM entries ORDER BY created_at DESC")
.fetch_all(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(entries))
}
pub async fn get_entry_content(
State(pool): State<PgPool>,
Path(id): Path<i32>,
) -> Result<String, StatusCode> {
let row: (String,) = sqlx::query_as("SELECT content FROM entries WHERE id = $1")
.bind(id)
.fetch_optional(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(row.0)
}
pub async fn create_entry(
State(pool): State<PgPool>,
Json(payload): Json<CreateEntry>,
) -> Result<(StatusCode, Json<Entry>), StatusCode> {
let entry = sqlx::query_as::<_, Entry>(
"INSERT INTO entries (content) VALUES ($1) RETURNING id, created_at, content",
)
.bind(&payload.content)
.fetch_one(&pool)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, Json(entry)))
}
pub async fn delete_entry(
State(pool): State<PgPool>,
Path(id): Path<i32>,
) -> StatusCode {
let result = sqlx::query("DELETE FROM entries WHERE id = $1")
.bind(id)
.execute(&pool)
.await;
match result {
Ok(r) if r.rows_affected() > 0 => StatusCode::NO_CONTENT,
Ok(_) => StatusCode::NOT_FOUND,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}