LCOV - code coverage report
Current view: top level - src - handler.rs (source / functions) Coverage Total Hit
Test: bliki.lcov Lines: 71.3 % 115 82
Test Date: 2025-11-27 15:46:07 Functions: 36.8 % 38 14

            Line data    Source code
       1              : use crate::responders::ResponseError;
       2              : use crate::{
       3              :     WebContext,
       4              :     article::{Article, ArticleCreateParams, ArticleUpdateParams, create_article, latest_articles},
       5              :     session::session_new,
       6              :     user::{User, verify_user},
       7              : };
       8              : use axum::body::Body;
       9              : use axum::response::Response;
      10              : use axum::{
      11              :     Form, Json,
      12              :     extract::{Multipart, Path, State},
      13              :     response::{Html, Redirect},
      14              : };
      15              : use axum_extra::extract::cookie::{Cookie, SignedCookieJar};
      16              : use http::{self, StatusCode};
      17              : use minijinja::context;
      18              : use serde::{Deserialize, Serialize};
      19              : 
      20              : #[cfg(test)]
      21              : mod tests;
      22              : 
      23            0 : pub(crate) async fn new_sign_in(
      24            0 :     State(state): State<WebContext>,
      25            0 : ) -> Result<Html<String>, ResponseError> {
      26            0 :     let tmpl = state.templates.get_template("new_sign_in.html")?;
      27            0 :     let content = tmpl.render(context!())?;
      28            0 :     Ok(Html(content))
      29            0 : }
      30              : 
      31              : #[derive(Debug, Deserialize)]
      32              : pub(crate) struct SignInParams {
      33              :     username: String,
      34              :     password: String,
      35              : }
      36              : 
      37              : #[axum::debug_handler]
      38            5 : pub(crate) async fn sign_in(
      39            5 :     State(state): State<WebContext>,
      40            5 :     jar: SignedCookieJar,
      41            5 :     Form(params): Form<SignInParams>,
      42            5 : ) -> Result<(SignedCookieJar, Redirect), ResponseError> {
      43              :     // - [X] Extract the username and password.
      44              :     // - [X] Checks their validity.
      45              :     // - If valid:
      46              :     //   - [x] create a session for the user
      47              :     //   - [x] store the session in a cookie
      48              :     //   - [ ] redirect the user to / or their redirect_to query parameter
      49              :     // - If invalid:
      50              :     //   - [x] return 401.
      51            5 :     let db = state.db.lock()?;
      52            5 :     let ret = verify_user(&db, params.username, params.password.as_bytes())?;
      53            5 :     if let Some(user) = ret {
      54            4 :         let session = session_new(&db, user.user_id)?;
      55              :         // FIXME: Add .secure(true) to restrict to HTTPS connections only
      56            4 :         let cookie =
      57            4 :             Cookie::build(("sid", session.session_id)).path("/").http_only(false).secure(false);
      58            4 :         Ok((jar.add(cookie), Redirect::to("/")))
      59              :     } else {
      60            1 :         Err(ResponseError::unauthorized())
      61              :     }
      62            5 : }
      63              : 
      64              : #[axum::debug_handler]
      65            1 : pub(crate) async fn index(
      66            1 :     State(state): State<WebContext>,
      67            1 :     user: Option<User>,
      68            1 : ) -> Result<Html<String>, ResponseError> {
      69            1 :     let tmpl = state.templates.get_template("index.html")?;
      70            1 :     let db = state.db.lock()?;
      71            1 :     let articles: Vec<Article> = latest_articles(&db)?;
      72            1 :     let content = tmpl.render(context!(latest_articles => articles, user => user))?;
      73            1 :     Ok(Html(content))
      74            1 : }
      75              : 
      76              : #[axum::debug_handler]
      77            1 : pub(crate) async fn new_article(
      78            1 :     State(state): State<WebContext>,
      79            1 :     user: User,
      80            1 : ) -> Result<Html<String>, ResponseError> {
      81            1 :     let tmpl = state.templates.get_template("new_article.html")?;
      82            1 :     let content = tmpl.render(context!(user => user))?;
      83            1 :     Ok(Html(content))
      84            1 : }
      85              : 
      86              : #[axum::debug_handler]
      87            1 : pub(crate) async fn article_create(
      88            1 :     State(state): State<WebContext>,
      89            1 :     user: User,
      90            1 :     Form(params): Form<ArticleCreateParams>,
      91            1 : ) -> Result<Redirect, ResponseError> {
      92            1 :     let db = state.db.lock()?;
      93            1 :     let article_id = create_article(&db, params, user.user_id)?;
      94            1 :     let uri = format!("/article/{}/", article_id);
      95            1 :     Ok(Redirect::to(&uri))
      96            1 : }
      97              : 
      98              : #[axum::debug_handler]
      99            2 : pub(crate) async fn article_show(
     100            2 :     State(state): State<WebContext>,
     101            2 :     Path(article_id): Path<i64>,
     102            2 :     user: Option<User>,
     103            2 : ) -> Result<Html<String>, ResponseError> {
     104            2 :     let db = state.db.lock()?;
     105            2 :     let article = crate::article::find_by_id(&db, article_id)?;
     106            1 :     let tmpl = state.templates.get_template("show_article.html")?;
     107            1 :     let content = tmpl.render(context!(article => article, user => user))?;
     108            1 :     Ok(Html(content))
     109            2 : }
     110              : 
     111              : #[axum::debug_handler]
     112            0 : pub(crate) async fn article_edit(
     113            0 :     State(state): State<WebContext>,
     114            0 :     Path(article_id): Path<i64>,
     115            0 :     user: User,
     116            0 : ) -> Result<Html<String>, ResponseError> {
     117            0 :     let db = state.db.lock()?;
     118            0 :     let article = crate::article::find_by_id(&db, article_id)?;
     119            0 :     let tmpl = state.templates.get_template("edit_article.html")?;
     120            0 :     let content = tmpl.render(context!(article => article, user => user))?;
     121            0 :     Ok(Html(content))
     122            0 : }
     123              : 
     124              : #[axum::debug_handler]
     125            0 : pub(crate) async fn article_update(
     126            0 :     State(state): State<WebContext>,
     127            0 :     user: User,
     128            0 :     Form(params): Form<ArticleUpdateParams>,
     129            0 : ) -> Result<Redirect, ResponseError> {
     130            0 :     let db = state.db.lock()?;
     131            0 :     crate::article::update_article(&db, &params, user.user_id)?;
     132            0 :     let uri = format!("/article/{}/", params.page_id);
     133            0 :     Ok(Redirect::to(&uri))
     134            0 : }
     135              : 
     136              : #[derive(Serialize)]
     137              : pub struct UploadResponse {
     138              :     file_id: i64,
     139              :     filename: String,
     140              :     url: String,
     141              : }
     142              : // TODO: Upload default body to 10MiB.
     143              : // Cf. https://docs.rs/axum/latest/axum/extract/struct.Multipart.html#large-files
     144              : // 2025-05-04T15:02:49.683196Z  INFO bliki::handler: field name: "media"
     145              : #[axum::debug_handler]
     146            1 : pub(crate) async fn upload(
     147            1 :     State(state): State<WebContext>,
     148            1 :     _user: User,
     149            1 :     mut multipart: Multipart,
     150            1 : ) -> Result<Json<UploadResponse>, ResponseError> {
     151              :     // We only have one entry.
     152            1 :     if let Some(field) = multipart.next_field().await? {
     153              :         // asset name == "media"; can we check the filename?. We can check the ContentType for the mimetype
     154            1 :         let _name = field.name().ok_or(ResponseError::server_error())?;
     155              :         // TODO: Response with 422 if filename or content_type is not present.
     156            1 :         let filename = field.file_name().ok_or(ResponseError::server_error())?.to_string();
     157            1 :         let content_type = field.content_type().ok_or(ResponseError::server_error())?.to_string();
     158            1 :         let data = field.bytes().await?;
     159            1 :         let db = state.db.lock()?;
     160            1 :         let file_id =
     161            1 :             crate::files::store_file(&db, state.upload_dir, data, filename.clone(), content_type)?;
     162            1 :         let url = format!("/media/{}", file_id);
     163            1 :         let payload = UploadResponse { file_id, filename, url };
     164            1 :         Ok(Json(payload))
     165              :     } else {
     166            0 :         Err(ResponseError {
     167            0 :             status: StatusCode::BAD_REQUEST,
     168            0 :             msg: "missing media file".to_string(),
     169            0 :             format: crate::responders::Format::Json,
     170            0 :         })
     171              :     }
     172            1 : }
     173              : 
     174              : #[axum::debug_handler]
     175            1 : pub(crate) async fn media_show(
     176            1 :     State(state): State<WebContext>,
     177            1 :     Path(file_id): Path<i64>,
     178            1 : ) -> Result<Response, ResponseError> {
     179            1 :     let db = state.db.lock()?;
     180            1 :     let media_file = crate::files::find_by_id(&db, file_id)?;
     181              :     // TODO: Stream the response instead
     182            1 :     let path = state.upload_dir.join(media_file.digest);
     183            1 :     let contents = std::fs::read(path)?;
     184            1 :     let body = Body::from(contents);
     185            1 :     let res = Response::builder().header("Content-Type", &media_file.content_type).body(body)?;
     186            1 :     Ok(res)
     187            1 : }
        

Generated by: LCOV version 2.0-1