quokka-admin 0.1.0

An admin panel for quokka
Documentation
use axum::{
    http::{Method, StatusCode},
    response::{IntoResponse, Redirect},
    Extension, Form,
};
use quokka::{
    extract::{Extensions, Session},
    handler::html::{FormDataStore, FormResponse, TemplateDataLoader},
    state::{FromState, ProvideState, Templating},
};

use crate::{
    data::{Severity, Toast},
    middleware::{LoginData, LoginProviders},
    service::page_loader::AdminErrorMessage,
    state::AdminState,
};

use super::AdminPageData;

pub const ADMIN_USER_SESSION_KEY: &str = "admin_user_session";

#[derive(Clone, FromState)]
pub struct AdminLoginPageLoader {
    #[from_state(
        bounds = "State: ProvideState<AdminState<State>> + 'static",
        builder = ProvideState::<AdminState<_>>::provide(state).title,
    )]
    title: String,
    #[from_state(
        bounds = "State: ProvideState<AdminState<State>> + 'static",
        builder = ProvideState::<AdminState<_>>::provide(state).login_providers,
    )]
    login_providers: LoginProviders,
    templating: Templating,
}

pub struct LoginPageData {}

impl<S: Send + Sync + 'static> TemplateDataLoader<S> for AdminLoginPageLoader {
    type Args = (Method, Extensions<Toast>);

    type Data = AdminPageData<()>;

    #[tracing::instrument(
        skip_all,
        target = "quokka_admin::service::page_loader::admin_login_page_loader::AdminLoginPageLoader"
    )]
    async fn load_data(&self, (method, mut toasts): Self::Args) -> quokka::Result<Self::Data> {
        if method == Method::POST {
            toasts.0.push(Severity::Critical.message(
                "Login failed",
                "The user could not be found or the password is invalid",
            ));
        }

        Ok(AdminPageData {
            title: self.title.clone(),
            subtitle: Some("Login".to_string()),
            page: (),
            navigation: Default::default(),
            sidebar_widgets: Default::default(),
            toasts: toasts.0,
            error: Default::default(),
        })
    }

    #[tracing::instrument(
        skip_all,
        target = "quokka_admin::service::page_loader::admin_login_page_loader::AdminLoginPageLoader"
    )]
    async fn render_error(&self, error: quokka::Error) -> impl axum::response::IntoResponse {
        tracing::error!(?error, "Caught error in AdminPageLoader::render_error");

        (
            StatusCode::from_u16(error.status_code)
                .unwrap_or_else(|_| StatusCode::INTERNAL_SERVER_ERROR),
            axum::response::Html(self.templating.render(
                "quokka-admin/page/login/error.html.hbs",
                &AdminPageData::<()> {
                    title: "Quokka Admin".to_string(),
                    navigation: Default::default(),
                    sidebar_widgets: Default::default(),
                    error: Some(AdminErrorMessage {
                        message: error.message,
                        status_code: error.status_code,
                    }),
                    ..Default::default()
                },
            )),
        )
    }
}

impl<S: Send + Sync + 'static> FormDataStore<S> for AdminLoginPageLoader {
    type StoreArgs = Extension<Session>;

    type AdditionalResponse = Session;

    type Body = Form<LoginData>;

    async fn store_data(
        &self,
        Extension(mut session): Self::StoreArgs,
        Form(form): Self::Body,
    ) -> quokka::Result<FormResponse<Self::AdditionalResponse>> {
        for provider in &self.login_providers.providers {
            match provider.login(&form).await {
                Ok(Some(data)) => {
                    let message = Extensions::new().add(Severity::Success.message(
                        "Login successful",
                        format!("Welcome back {}", data.user_identifier),
                    ));

                    tracing::debug!(
                        ?data,
                        provider = provider.provider_name(),
                        "User authenticated"
                    );

                    session
                        .add_extension(ADMIN_USER_SESSION_KEY, data)
                        .inspect_err(|error| {
                            tracing::error!(
                                ?error,
                                "Error while adding login result to user session"
                            )
                        })
                        .ok();

                    return Ok(FormResponse::Exit(
                        (session, message, Redirect::to("/admin")).into_response(),
                    ));
                }
                Err(error) => {
                    tracing::error!(
                        ?error,
                        provider = provider.provider_name(),
                        "Error while logging in user"
                    );
                }
                _ => {}
            }
        }

        Ok(FormResponse::Empty)
    }
}