240 lines
7.0 KiB
Rust
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(
|
|
¶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::<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);
|
|
}
|
|
}
|