ordinary-app 0.10.0

Application server for Ordinary
// Copyright (C) 2026 Ordinary Labs, LLC.
//
// SPDX-License-Identifier: AGPL-3.0-only

use crate::server::middleware::apply_custom_to_method_router;
use crate::server::{OrdinaryAppRouter, OrdinaryAppServerState, cors, ops};
use axum::Router;
use axum::http::StatusCode;
use axum::routing::get;
use hashbrown::HashMap;
use ordinary_action::Engine;
use ordinary_auth::Auth;
use ordinary_config::{ContentDefinition, ModelConfig, OrdinaryConfig, TemplateFfiVersion};
use ordinary_storage::{ArtifactKind, Storage};
use ordinary_template::OrdinaryTemplate;
use std::sync::Arc;
use std::time::Duration;
use tower::ServiceBuilder;
use tower_http::timeout::TimeoutLayer;

#[allow(clippy::too_many_lines, clippy::type_complexity)]
pub(crate) async fn setup(
    config: &mut OrdinaryConfig,
    auth: &Arc<Auth>,
    storage: &Arc<Storage>,
    secure: bool,
    engine: &Engine,
    model_map: &mut HashMap<String, ModelConfig>,
    content_map: &mut HashMap<String, ContentDefinition>,
) -> (
    Vec<OrdinaryTemplate>,
    HashMap<String, usize>,
    Option<u8>,
    Option<u8>,
) {
    let template_ct = match &config.templates {
        Some(i) => i.len(),
        None => 0,
    };

    let mut templates = Vec::with_capacity(template_ct);
    let mut template_route_map = HashMap::with_capacity(template_ct);

    let mut error_template_idx = None;
    let mut mfa_totp_template_idx = None;

    let mut globals_map = HashMap::new();

    if let Some(globals) = &config.globals {
        for global in globals {
            globals_map.insert(global.name.clone(), global.clone());
        }
    }

    if let Some(mut template_configs) = config.templates.clone()
        && !template_configs.is_empty()
    {
        template_configs.sort_by_key(|a| a.idx);

        for (i, template_config) in template_configs.iter().enumerate() {
            let template_idx = template_config.idx_validated();
            let template_name = template_config.name_validated();

            if i != template_idx as usize {
                tracing::error!("gap in or duplicate template indexes");
            }

            let is_totp = if let Some(auth) = &config.auth
                && let Some(totp_template) = &auth.mfa.totp.template
                && template_name == totp_template
            {
                mfa_totp_template_idx = Some(template_idx);
                true
            } else {
                false
            };

            let is_error = if let Some(error) = &config.error
                && let Some(error_template_name) = &error.template
                && template_name == error_template_name
            {
                error_template_idx = Some(template_idx);
                true
            } else {
                false
            };

            let artifact_span = tracing::info_span!("artifact");

            let src = artifact_span.in_scope(|| {
                let artifact_kind = match template_config.ffi_validated().version {
                    TemplateFfiVersion::V1 => ArtifactKind::TemplateWasiP1,
                    TemplateFfiVersion::V2 => ArtifactKind::TemplateWasiP2,
                };

                match storage.artifact.get(template_idx, artifact_kind) {
                    Ok(val) => Some(val),
                    Err(err) => {
                        tracing::warn!(
                            name = template_config.name,
                            %err,
                            "template not stored",
                        );
                        None
                    }
                }
            });

            let mut globals_list = vec![];

            if let Some(template_globals) = &template_config.globals {
                for template_global in template_globals {
                    if let Some(global) = globals_map.get(template_global) {
                        globals_list.push(global.clone());
                    }
                }
            }

            if let Ok(template) = OrdinaryTemplate::new(
                &config.domain,
                &config.version,
                &config.canonical.clone().unwrap_or(
                    config
                        .cnames
                        .clone()
                        .unwrap_or(vec![config.domain.clone()])
                        .first()
                        .unwrap_or(&config.domain)
                        .clone(),
                ),
                if globals_list.is_empty() {
                    None
                } else {
                    Some(globals_list)
                },
                secure,
                is_error,
                is_totp,
                src,
                template_config.clone(),
                auth.clone(),
                model_map,
                content_map,
                engine.clone(),
                storage.clone(),
            )
            .await
            {
                templates.push(template);
            }
        }

        for template in &templates {
            let template_route = template.config.route_validated().to_string();

            template_route_map.insert(template_route.clone(), template.idx as usize);
        }
    }

    (
        templates,
        template_route_map,
        error_template_idx,
        mfa_totp_template_idx,
    )
}

pub(crate) fn setup_router(
    config: &Arc<OrdinaryConfig>,
    state: &Arc<OrdinaryAppServerState>,
    api_domain: Option<&str>,
    forwarded_by: &str,
    forwarded_proto: &str,
) -> Option<OrdinaryAppRouter> {
    if let Some(template_configs) = &config.templates
        && !template_configs.is_empty()
    {
        let mut router = Router::new();

        for template_config in template_configs {
            // todo: default for default should come from Ordinary API
            let timeout_s = u64::from(
                template_config
                    .timeout
                    .unwrap_or(config.default_timeout.unwrap_or(10)),
            );

            let mut route = get(ops::templates::get).route_layer(ServiceBuilder::new().layer(
                TimeoutLayer::with_status_code(
                    StatusCode::REQUEST_TIMEOUT,
                    Duration::from_secs(timeout_s),
                ),
            ));

            if let Some(names) = &template_config.middlewares {
                route = apply_custom_to_method_router(
                    route,
                    config,
                    state,
                    names,
                    config.domain.clone(),
                    forwarded_by.to_string(),
                    forwarded_proto.to_string(),
                    api_domain,
                );
            }

            route = cors::apply_to_route(config.cors.as_ref(), config.cors.as_ref(), route);
            router = router.route(template_config.route_validated(), route);
        }

        return Some(router);
    }

    None
}