aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorXe Iaso <me@christine.website>2022-07-11 11:16:54 +0000
committerXe Iaso <me@christine.website>2022-07-11 11:16:54 +0000
commit4e57715b09c064d9729727a6d28847242cdb93a7 (patch)
tree10a9f95033fb0723a78185d1286bececf3fb5840 /src
parentb0a87b890e2f97842ff738166207341ba5f11e58 (diff)
downloadxesite-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.rs2
-rw-r--r--src/handlers/mod.rs11
-rw-r--r--src/handlers/notes.rs243
-rw-r--r--src/main.rs5
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))