use { crate::{ authentication::{ authenticate_user_with_jwt, authenticate_user_with_password, check_if_user_is_admin, create_jwt_for_user, AuthenticatedAdminUser, ParsedJwt, Password, }, db::Database, error::Error, }, askama::Template, askama_axum::{IntoResponse, Response}, axum::{ extract::{NestedPath, State}, response::Redirect, routing::{get, post}, Form, Router, }, axum_extra::extract::{ cookie::{Cookie, SameSite}, CookieJar, }, serde::Deserialize, }; use super::AppState; pub fn routes() -> Router> { Router::new() .route("/", get(root)) .route("/create_first_admin_user", get(get_create_first_admin_user)) .route( "/create_first_admin_user", post(post_create_first_admin_user), ) } #[derive(Template)] #[template(path = "admin/create_first_user.html")] struct CreateFirstUserTemplate {} #[derive(Template)] #[template(path = "admin/first_login.html")] struct FirstLoginTemplate {} #[derive(Template)] #[template(path = "admin/index.html")] struct IndexTemplate<'a> { admin_user_name: &'a str, } #[tracing::instrument] async fn check_jwt( db: &D, cookie_jar: &CookieJar, ) -> Result { match authenticate_user_with_jwt( db, cookie_jar .get("jwt") .map(|cookie| cookie.value_trimmed()) .ok_or(Error::new_unauthorized())?, ) .await? { ParsedJwt::Valid(user) => check_if_user_is_admin(db, &user) .await? .ok_or(Error::new_unauthorized()), ParsedJwt::InvalidSignature => Err(Error::new_unauthorized()), ParsedJwt::UserNotFound => Err(Error::new_unauthorized()), ParsedJwt::Expired(user) => Err(Error::new_jwt_expired(user)), } } #[tracing::instrument] async fn root( cookie_jar: CookieJar, State(AppState { db, .. }): State>, path: NestedPath, ) -> Result { Ok(if !db.has_admin_users().await? { Redirect::temporary(&format!("{}/create_first_admin_user", path.as_str())).into_response() } else { let admin_user = check_jwt(&db, &cookie_jar).await?; IndexTemplate { admin_user_name: &admin_user.real_name, } .into_response() }) } #[tracing::instrument] async fn get_create_first_admin_user() -> CreateFirstUserTemplate { CreateFirstUserTemplate {} } #[derive(Deserialize, Debug)] struct CreateFirstUserParameters { real_name: String, email: String, password: String, } #[tracing::instrument] async fn post_create_first_admin_user( cookie_jar: CookieJar, State(AppState:: { db, .. }): State>, Form(params): Form, ) -> Result<(CookieJar, FirstLoginTemplate), Error> { let user = db .create_first_admin_user( ¶ms.real_name, ¶ms.email, &Password::new(¶ms.password)?.into(), ) .await?; let user = authenticate_user_with_password(&db, user, ¶ms.password) .await? .ok_or(Error::new_unexpected( "Could not authenticate newly-created user.", ))?; Ok(( cookie_jar .add(Cookie::build(("jwt", create_jwt_for_user(&user)?)).same_site(SameSite::Strict)), FirstLoginTemplate {}, )) } #[cfg(test)] mod tests { use super::*; use crate::{app::AppState, db::fake::FakeDatabase}; use { axum::{ body, body::Body, http::{Request, StatusCode}, }, scraper::{Html, Selector}, tower::{Service, ServiceExt}, }; #[tokio::test] async fn root_redirects_when_no_admin_users() { let app = Router::new() .nest("/test_admin", routes()) .with_state(AppState { db: FakeDatabase::new_empty(), }); let response = app .oneshot( Request::builder() .method(http::Method::GET) .uri("/test_admin") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::TEMPORARY_REDIRECT); assert!(response.headers().contains_key("location")); assert_eq!( "/test_admin/create_first_admin_user", response.headers()["location"] ); } #[tokio::test] async fn create_first_admin_user() { let mut app = Router::new() .nest("/test_admin", routes()) .with_state(AppState { db: FakeDatabase::new_empty(), }) .into_service(); let request = Request::get("/test_admin/create_first_admin_user") .body(Body::empty()) .unwrap(); let response = ServiceExt::>::ready(&mut app) .await .unwrap() .call(request) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let body = body::to_bytes(response.into_body(), 10000).await.unwrap(); let html = Html::parse_document(&String::from_utf8(body.into()).unwrap()); let form_selector = Selector::parse("form").unwrap(); let mut form_elements = html.select(&form_selector); let form_element = form_elements.next().unwrap(); assert_eq!(0, form_elements.count()); assert_eq!(Some("create_first_admin_user"), form_element.attr("action")); assert_eq!(Some("post"), form_element.attr("method")); let input_selector = Selector::parse("input").unwrap(); let inputs: Vec<_> = form_element.select(&input_selector).collect(); assert_eq!( 1, inputs .iter() .filter(|elem| elem.attr("name") == Some("real_name")) .count() ); assert_eq!( 1, inputs .iter() .filter(|elem| elem.attr("name") == Some("email")) .count() ); assert_eq!( 1, inputs .iter() .filter(|elem| elem.attr("name") == Some("password")) .filter(|elem| elem.attr("type") == Some("password")) .count() ); let request = Request::post("/test_admin/create_first_admin_user") .header( http::header::CONTENT_TYPE, "application/x-www-form-urlencoded", ) .body(Body::from( "real_name=Joe%20User&email=joe%40user.com&password=abc123", )) .unwrap(); let response = ServiceExt::>::ready(&mut app) .await .unwrap() .call(request) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } }