locality/src/app/admin.rs

240 lines
7.0 KiB
Rust

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<D: Database>() -> Router<AppState<D>> {
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<D: Database>(
db: &D,
cookie_jar: &CookieJar,
) -> Result<AuthenticatedAdminUser, Error> {
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<D: Database>(
cookie_jar: CookieJar,
State(AppState { db, .. }): State<AppState<D>>,
path: NestedPath,
) -> Result<Response, Error> {
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<D: Database>(
cookie_jar: CookieJar,
State(AppState::<D> { db, .. }): State<AppState<D>>,
Form(params): Form<CreateFirstUserParameters>,
) -> Result<(CookieJar, FirstLoginTemplate), Error> {
let user = db
.create_first_admin_user(
&params.real_name,
&params.email,
&Password::new(&params.password)?.into(),
)
.await?;
let user = authenticate_user_with_password(&db, user, &params.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::<Request<Body>>::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::<Request<Body>>::ready(&mut app)
.await
.unwrap()
.call(request)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}