diff options
| author | Xe Iaso <me@christine.website> | 2022-07-11 11:16:54 +0000 |
|---|---|---|
| committer | Xe Iaso <me@christine.website> | 2022-07-11 11:16:54 +0000 |
| commit | 4e57715b09c064d9729727a6d28847242cdb93a7 (patch) | |
| tree | 10a9f95033fb0723a78185d1286bececf3fb5840 /src | |
| parent | b0a87b890e2f97842ff738166207341ba5f11e58 (diff) | |
| download | xesite-4e57715b09c064d9729727a6d28847242cdb93a7.tar.xz xesite-4e57715b09c064d9729727a6d28847242cdb93a7.zip | |
handlers/notes: create, update, delete, tailauth too
Signed-off-by: Xe Iaso <me@christine.website>
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/mod.rs | 2 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 11 | ||||
| -rw-r--r-- | src/handlers/notes.rs | 243 | ||||
| -rw-r--r-- | src/main.rs | 5 |
4 files changed, 244 insertions, 17 deletions
diff --git a/src/app/mod.rs b/src/app/mod.rs index 24eabc5..8ebe152 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -77,7 +77,7 @@ pub async fn init(cfg: PathBuf) -> Result<State> { let talks = crate::post::load(cfg.clone(), "talks").await?; let mut everything: Vec<Post> = vec![]; let mgr = RusqliteConnectionManager::new( - env::var("DATABASE_URL").unwrap_or("./var/waifud.db".to_string()), + env::var("DATABASE_URL").unwrap_or("./xesite.db".to_string()), ); let pool = bb8::Pool::builder().build(mgr).await?; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 87175ca..c3264ec 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -8,7 +8,7 @@ use axum::{ use chrono::{Datelike, Timelike, Utc, Weekday}; use lazy_static::lazy_static; use prometheus::{opts, register_int_counter_vec, IntCounterVec}; -use std::sync::Arc; +use std::{net::AddrParseError, sync::Arc}; use tracing::instrument; pub mod api; @@ -172,6 +172,15 @@ pub enum Error { #[error("database pool error: {0}")] SQLitePool(#[from] bb8_rusqlite::Error), + #[error("address parse error: {0}")] + AddrParse(#[from] AddrParseError), + + #[error("Tailscale localapi error: {0}")] + TSLocalAPI(#[from] ts_localapi::Error), + + #[error("{0}")] + Eyre(#[from] color_eyre::eyre::ErrReport), + #[error("other error: {0}")] Catchall(String), } diff --git a/src/handlers/notes.rs b/src/handlers/notes.rs index 56e0f90..60d82f7 100644 --- a/src/handlers/notes.rs +++ b/src/handlers/notes.rs @@ -1,10 +1,14 @@ -use crate::templates; -use axum::{extract::Path, response::Html, Json}; +use std::{net::SocketAddr, sync::Arc}; + +use crate::{app::State, templates}; +use axum::{extract::Path, http::HeaderMap, response::Html, Extension, Json}; use chrono::prelude::*; use maud::{html, Markup, PreEscaped}; use rusqlite::params; use serde::{Deserialize, Serialize}; +use super::Error; + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Note { pub id: u64, @@ -16,15 +20,49 @@ pub struct Note { pub reply_to: Option<String>, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NewNote { + pub content: String, + pub reply_to: Option<String>, +} + +impl Default for Note { + fn default() -> Self { + Self { + id: 0, + content: "".into(), + content_html: "".into(), + created_at: Utc::now(), + updated_at: None, + deleted_at: None, + reply_to: None, + } + } +} + impl Note { pub fn to_html(&self) -> Markup { html! { article."h-entry" { + a href={"/notes/" (self.id)} { + "🔗" + } + " " time."dt-published" datetime=(self.created_at) { {(self.detrytemci())} } - a href={"/notes/" (self.id)} { - "🔗" + " " + @if let Some(_updated_at) = &self.updated_at { + "📝 " + (self.update_detrytemci().unwrap()) + } + + @if let Some(deleted_at) = &self.deleted_at { + p { + " ⚠️ This post was deleted at " + (deleted_at.to_rfc3339()) + ". Please do not treat this note as a genuine expression of my views or opinions." + } } @if let Some(reply_to) = &self.reply_to { @@ -43,7 +81,21 @@ impl Note { } pub fn detrytemci(&self) -> String { - self.created_at.format("M%m %d %Y %M:%H").to_string() + self.created_at.format("M%m %d %Y %H:%M").to_string() + } + + pub fn update_detrytemci(&self) -> Option<String> { + if self.updated_at.is_none() { + return None; + } + + Some( + self.updated_at + .as_ref() + .unwrap() + .format("M%m %d %Y %H:%M") + .to_string(), + ) } } @@ -70,9 +122,9 @@ impl Into<xe_jsonfeed::Item> for Note { } } -#[instrument(err)] -pub async fn index() -> super::Result { - let conn = crate::establish_connection()?; +#[instrument(err, skip(state))] +pub async fn index(Extension(state): Extension<Arc<State>>) -> super::Result { + let conn = state.pool.get().await?; let mut stmt = conn.prepare("SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 25")?; let notes = stmt @@ -96,9 +148,11 @@ pub async fn index() -> super::Result { Ok(Html(result)) } -#[instrument(err)] -pub async fn feed() -> super::Result<Json<xe_jsonfeed::Feed>> { - let conn = crate::establish_connection()?; +#[instrument(err, skip(state))] +pub async fn feed( + Extension(state): Extension<Arc<State>>, +) -> super::Result<Json<xe_jsonfeed::Feed>> { + let conn = state.pool.get().await?; let mut stmt = conn.prepare("SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes WHERE deleted_at IS NULL ORDER BY id DESC LIMIT 25")?; let notes = stmt @@ -135,9 +189,9 @@ pub async fn feed() -> super::Result<Json<xe_jsonfeed::Feed>> { Ok(Json(feed.build())) } -#[instrument(err)] -pub async fn view(Path(id): Path<u64>) -> super::Result { - let conn = crate::establish_connection()?; +#[instrument(err, skip(state))] +pub async fn view(Extension(state): Extension<Arc<State>>, Path(id): Path<u64>) -> super::Result { + let conn = state.pool.get().await?; let mut stmt = conn.prepare( "SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes WHERE id = ?1" @@ -159,3 +213,164 @@ pub async fn view(Path(id): Path<u64>) -> super::Result { templates::notepost_html(&mut result, note)?; Ok(Html(result)) } + +#[instrument(err, skip(state, headers))] +pub async fn delete( + Extension(state): Extension<Arc<State>>, + Path(id): Path<u64>, + headers: HeaderMap, +) -> super::Result<String> { + let conn = state.pool.get().await?; + + let ip = headers.get("X-Real-Ip").clone(); + + if ip.is_none() { + return Err(Error::Catchall("Cannot get X-Real-Ip header".into())); + } + + let ip: SocketAddr = (ip.unwrap().to_str()?.to_owned() + ":0").parse()?; + let whois = ts_localapi::whois(ip).await?; + + if whois.user_profile.login_name != "Xe@github" { + return Err(Error::Catchall(format!( + "expected Tailscale user Xe@github, got: {}", + whois.user_profile.login_name + ))); + } + + conn.execute( + "UPDATE notes SET deleted_at=?2 WHERE id=?1", + params![id, Utc::now().to_rfc3339()], + )?; + + Ok("deleted".into()) +} + +#[instrument(err, skip(state, headers))] +pub async fn update( + Extension(state): Extension<Arc<State>>, + Path(id): Path<u64>, + headers: HeaderMap, + data: Json<NewNote>, +) -> super::Result<Json<Note>> { + let conn = state.pool.get().await?; + + let ip = headers.get("X-Real-Ip").clone(); + + if ip.is_none() { + return Err(Error::Catchall("Cannot get X-Real-Ip header".into())); + } + + let ip: SocketAddr = (ip.unwrap().to_str()?.to_owned() + ":0").parse()?; + let whois = ts_localapi::whois(ip).await?; + + if whois.user_profile.login_name != "Xe@github" { + return Err(Error::Catchall(format!( + "expected Tailscale user Xe@github, got: {}", + whois.user_profile.login_name + ))); + } + + info!( + "authenticated as {} from machine {}", + whois.user_profile.login_name, whois.node.hostinfo.hostname, + ); + + let content_html = crate::app::markdown::render(state.clone().cfg.clone(), &data.content)?; + + let mut stmt = conn.prepare( + "SELECT id, content, content_html, created_at, updated_at, deleted_at, reply_to FROM notes WHERE id = ?1" + )?; + + let old_note = stmt.query_row(params![id], |row| { + Ok(Note { + id: row.get(0)?, + content: row.get(1)?, + content_html: row.get(2)?, + created_at: row.get(3)?, + updated_at: row.get(4)?, + deleted_at: row.get(5)?, + reply_to: row.get(6)?, + }) + })?; + + let mut note = Note { + content: data.content.clone(), + content_html, + created_at: old_note.created_at, + updated_at: Some(Utc::now()), + reply_to: old_note.reply_to, + ..Default::default() + }; + + conn.execute( + "UPDATE notes SET (content, content_html, created_at, updated_at, deleted_at, reply_to) VALUES(?, ?, ?, ?, ?, ?)", + params![ + note.content, + note.content_html, + note.created_at, + note.updated_at, + note.deleted_at, + note.reply_to + ], + )?; + + note.id = conn.last_insert_rowid() as u64; + + Ok(Json(note)) +} + +#[instrument(err, skip(state, headers))] +pub async fn create( + Extension(state): Extension<Arc<State>>, + headers: HeaderMap, + data: Json<NewNote>, +) -> super::Result<Json<Note>> { + let conn = state.pool.get().await?; + + let ip = headers.get("X-Real-Ip").clone(); + + if ip.is_none() { + return Err(Error::Catchall("Cannot get X-Real-Ip header".into())); + } + + let ip: SocketAddr = (ip.unwrap().to_str()?.to_owned() + ":0").parse()?; + let whois = ts_localapi::whois(ip).await?; + + if whois.user_profile.login_name != "Xe@github" { + return Err(Error::Catchall(format!( + "expected Tailscale user Xe@github, got: {}", + whois.user_profile.login_name + ))); + } + + info!( + "authenticated as {} from machine {}", + whois.user_profile.login_name, whois.node.hostinfo.hostname, + ); + + let content_html = crate::app::markdown::render(state.clone().cfg.clone(), &data.content)?; + + let mut note = Note { + content: data.content.clone(), + content_html, + reply_to: data.reply_to.clone(), + ..Default::default() + }; + + conn.execute( + "INSERT INTO notes(content, content_html, created_at, updated_at, deleted_at, reply_to) VALUES(?, ?, ?, ?, ?, ?)", + params![ + note.content, + note.content_html, + note.created_at, + note.updated_at, + note.deleted_at, + note.reply_to + ], + )?; + + note.id = conn.last_insert_rowid() as u64; + + Ok(Json(note)) +} diff --git a/src/main.rs b/src/main.rs index 76196a8..736bbfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use axum::{ extract::Extension, http::header::{self, HeaderValue, CONTENT_TYPE}, response::{Html, Response}, - routing::{get, get_service}, + routing::{delete, get, get_service, post}, Router, }; use color_eyre::eyre::Result; @@ -199,6 +199,9 @@ async fn main() -> Result<()> { .route("/talks/", get(handlers::talks::index)) .route("/talks/:name", get(handlers::talks::post_view)) // notes + .route("/api/notes/create", post(handlers::notes::create)) + .route("/api/notes/:id", delete(handlers::notes::delete)) + .route("/api/notes/:id/update", post(handlers::notes::update)) .route("/notes", get(handlers::notes::index)) .route("/notes.json", get(handlers::notes::feed)) .route("/notes/:id", get(handlers::notes::view)) |
