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:
1
backend/.gitignore
vendored
Normal file
1
backend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/target
|
||||
2065
backend/Cargo.lock
generated
Normal file
2065
backend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
backend/Cargo.toml
Normal file
13
backend/Cargo.toml
Normal 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
14
backend/src/db.rs
Normal 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
34
backend/src/main.rs
Normal 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
80
backend/src/routes.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user