Blog attempt 5
at trunk 282 lines 8.9 kB view raw
1use chrono::DateTime; 2use maud::{Markup, PreEscaped, html}; 3use poem::{ 4 IntoResponse as _, Response, handler, 5 web::{Data, Form, Json, Redirect, cookie::CookieJar}, 6}; 7use serde::Deserialize; 8 9use super::{ 10 ISO8601_DATE, Post, clean_empty_string, 11 fetch::{one, one_draft}, 12 parse_date, read_secret, 13}; 14use crate::{ 15 D, W, 16 error::{AppError, Result}, 17 post, 18 template::header_extra, 19}; 20 21struct PostData { 22 bsky_uri: Option<String>, 23 category: Option<String>, 24 contents: String, 25 creation_datetime: DateTime<chrono::Local>, 26 slug: String, 27 subtitle: Option<String>, 28 title: String, 29} 30 31#[derive(Copy, Clone)] 32enum PostOperation { 33 Insert, 34 Update, 35 UpdateDraft, 36} 37 38#[derive(Deserialize)] 39pub struct SubmitNewPostForm { 40 pub bsky_uri: Option<String>, 41 pub category: Option<String>, 42 pub contents: String, 43 pub creation_datetime: String, 44 pub slug: String, 45 pub subtitle: Option<String>, 46 pub title: String, 47} 48 49#[derive(Deserialize)] 50pub struct RenderDraftForm { 51 pub contents: String, 52} 53 54#[derive(Deserialize)] 55pub struct SubmitEditedPostForm { 56 pub bsky_uri: Option<String>, 57 pub category: Option<String>, 58 pub contents: String, 59 pub creation_datetime: String, 60 pub slug: String, 61 pub subtitle: Option<String>, 62 pub title: String, 63} 64 65fn check_auth(cookie_jar: &CookieJar) -> Result<()> { 66 let secret = read_secret(); 67 match cookie_jar.get("secret_pass") { 68 Some(cookie) if cookie.value_str() == secret => Ok(()), 69 _ => Err(AppError::Unauthorized), 70 } 71} 72 73#[handler] 74pub fn login(body: String, cookie_jar: &CookieJar) -> Result<Response> { 75 cookie_jar.add(poem::web::cookie::Cookie::new_with_str("secret_pass", body)); 76 let secret_value = cookie_jar 77 .get("secret_pass") 78 .ok_or(AppError::internal_server_error( 79 "Cookie should exist after being set".to_owned(), 80 ))?; 81 Ok(html! {(secret_value)}.into_response()) 82} 83 84#[handler] 85pub fn new_post(cookie_jar: &CookieJar, Data(conn): D<&W>) -> Result<Response> { 86 check_auth(cookie_jar)?; 87 88 let post = one_draft(conn)?; 89 90 let response = render_post_form(&post, "POST", "/blog/new", "new"); 91 Ok(response.into_response()) 92} 93 94#[handler] 95pub fn edit_post( 96 cookie_jar: &CookieJar, 97 poem::web::Path(slug): poem::web::Path<String>, 98 Data(conn): D<&W>, 99) -> Result<Response> { 100 check_auth(cookie_jar)?; 101 102 let post = one(slug.to_string(), conn)?; 103 let response = render_post_form(&post, "POST", &format!("/blog/edit/{slug}"), "edit"); 104 Ok(response.into_response()) 105} 106 107fn render_post_form(post: &Post, method: &str, action: &str, js_mode: &str) -> Markup { 108 html! { 109 ( header_extra(&html! { 110 script defer src="/static/js/footnotes.js" {} 111 }) ) 112 body { 113 form method=(method) action=(action) { 114 #editorWrapper { 115 .editorInnerWrapper { 116 .editorInputs { 117 details { 118 summary { "Options" } 119 .contents { 120 label { 121 "Title: " 122 input name="title" value=(post.title.clone()) {} 123 } 124 label { 125 "Slug: " 126 input name="slug" value=(post.slug.clone()) {} 127 } 128 label { 129 "dtl: " 130 input name="creation_datetime" type="datetime-local" value=(post.creation_datetime.format(ISO8601_DATE)) {} 131 } 132 label { 133 "Subtitle: " 134 input name="subtitle" value=[post.subtitle.clone()] {} 135 } 136 label { 137 "Category: " 138 input name="category" value=[post.category.clone()] {} 139 } 140 label { 141 span {"bsky_uri: " a href="https://pdsls.dev" {"pdsls"}} 142 input name="bsky_uri" value=[post.bsky_uri.clone()] {} 143 } 144 label { 145 span { "Publish!" } 146 button type="submit" disabled style="display: none" aria-hidden="true"; 147 button type="submit" { "Publish" } 148 } 149 } 150 151 } 152 } 153 textarea #editor name="contents" { (post.contents.clone()) } 154 } 155 #editorPreview { "hii :3" } 156 } 157 } 158 159 script type="module" { 160 (PreEscaped(format!(r#" 161 import {{ initPostEditor }} from "/static/js/post-editor.js"; 162 initPostEditor("{js_mode}"); 163 "#))) 164 } 165 } 166 } 167} 168 169#[handler] 170pub fn submit_new_post( 171 cookie_jar: &CookieJar, 172 Form(form): Form<SubmitNewPostForm>, 173 Data(conn): D<&W>, 174) -> Result<Response> { 175 check_auth(cookie_jar)?; 176 177 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local); 178 179 let post_data = PostData { 180 title: form.title, 181 contents: form.contents, 182 slug: form.slug.clone(), 183 subtitle: form.subtitle, 184 category: form.category, 185 bsky_uri: form.bsky_uri, 186 creation_datetime, 187 }; 188 189 save_post(conn, &post_data, PostOperation::Insert)?; 190 191 Ok(Redirect::see_other(format!("/blog/{}", form.slug)).into_response()) 192} 193 194#[handler] 195pub fn submit_edited_post( 196 cookie_jar: &CookieJar, 197 Form(form): Form<SubmitEditedPostForm>, 198 Data(conn): D<&W>, 199) -> Result<Response> { 200 check_auth(cookie_jar)?; 201 202 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local); 203 204 let post_data = PostData { 205 title: form.title, 206 contents: form.contents, 207 slug: form.slug.clone(), 208 subtitle: form.subtitle, 209 category: form.category, 210 bsky_uri: form.bsky_uri, 211 creation_datetime, 212 }; 213 214 save_post(conn, &post_data, PostOperation::Update)?; 215 216 Ok(Redirect::see_other(format!("/blog/{}", form.slug)).into_response()) 217} 218 219#[handler] 220pub fn update_draft( 221 cookie_jar: &CookieJar, 222 Json(form): Json<SubmitNewPostForm>, 223 Data(conn): D<&W>, 224) -> Result<Response> { 225 check_auth(cookie_jar)?; 226 227 let creation_datetime = parse_date(&form.creation_datetime).with_timezone(&chrono::Local); 228 229 let post_data = PostData { 230 title: form.title, 231 contents: form.contents.clone(), 232 slug: form.slug, 233 subtitle: form.subtitle, 234 category: form.category, 235 bsky_uri: form.bsky_uri, 236 creation_datetime, 237 }; 238 239 save_post(conn, &post_data, PostOperation::UpdateDraft)?; 240 241 let rendered = post::render::render(&form.contents)?; 242 Ok(rendered.into_response()) 243} 244 245#[handler] 246pub fn render_draft(cookie_jar: &CookieJar, Json(form): Json<RenderDraftForm>) -> Result<Response> { 247 check_auth(cookie_jar)?; 248 249 let rendered = post::render::render(&form.contents)?; 250 Ok(rendered.into_response()) 251} 252 253fn save_post(conn: &W, post_data: &PostData, operation: PostOperation) -> Result<()> { 254 let conn = conn.lock()?; 255 256 let query = match operation { 257 PostOperation::Insert => { 258 "INSERT INTO post (title, contents, slug, subtitle, category, bsky_uri, creation_datetime) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" 259 } 260 PostOperation::Update => { 261 "UPDATE post SET title = ?1, contents = ?2, slug = ?3, subtitle = ?4, category = ?5, bsky_uri = ?6, creation_datetime = ?7 WHERE slug = ?3" 262 } 263 PostOperation::UpdateDraft => { 264 "UPDATE draft SET title = ?1, contents = ?2, slug = ?3, subtitle = ?4, category = ?5, bsky_uri = ?6, creation_datetime = ?7" 265 } 266 }; 267 268 let mut stmt = conn.prepare(query).map_err(AppError::from)?; 269 270 stmt.execute(( 271 &post_data.title, 272 &post_data.contents, 273 &post_data.slug, 274 clean_empty_string(post_data.subtitle.clone()), 275 clean_empty_string(post_data.category.clone()), 276 clean_empty_string(post_data.bsky_uri.clone()), 277 post_data.creation_datetime, 278 )) 279 .map_err(AppError::from)?; 280 281 Ok(()) 282}