ordinary-api 0.6.0-pre.13

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

use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum_extra::extract::Query;

use crate::server::APPLICATION;
use serde::Deserialize;
use std::sync::Arc;
use tracing::Instrument;
use utoipa::IntoParams;

#[derive(Deserialize, IntoParams)]
pub struct Params {
    /// project domain
    d: String,
    /// template index in the ordinary.json
    i: u8,
    /// content security policy inline style hashes
    #[serde(default)]
    style_csp: Vec<String>,
    /// content security policy inline script hashes
    #[serde(default)]
    script_csp: Vec<String>,
}

#[utoipa::path(
    put,
    path = "/templates",
    tag = APPLICATION,
    request_body(content = [u8], content_type = "application/octet-stream"),
    params(Params),
    responses(
        (status = 401, description = "unauthorized for operation"),
        (status = 200, description = "upload a template for an application"),
    ),
    security(
        ("access" = []),
    ),
)]
pub async fn upload(
    State(state): State<Arc<crate::server::OrdinaryApiServerState>>,
    Query(Params {
        d,
        i,
        style_csp,
        script_csp,
    }): Query<Params>,
    headers: HeaderMap,
    body: Bytes,
) -> impl IntoResponse {
    let domain = d;
    let idx = i;

    let span = tracing::info_span!("app", %domain);
    let span = span.in_scope(|| tracing::info_span!("template", idx));
    let span = span.in_scope(|| tracing::info_span!("upload"));

    async {
        match crate::server::check_ordinary_auth(&state, &headers, 4, &domain) {
            Ok(account) => account,
            Err(code) => return code.into_response(),
        };

        tracing::info!("uploading");

        let apps = state.apps.read().await;

        if let Some(wrapped_app) = apps.get(&domain) {
            match wrapped_app
                .app
                .set_template(
                    idx,
                    &body[..],
                    (!style_csp.is_empty()).then_some(style_csp),
                    (!script_csp.is_empty()).then_some(script_csp),
                    state.secure,
                )
                .await
            {
                Ok(name) => {
                    tracing::info!(name, "uploaded");
                    return StatusCode::OK.into_response();
                }
                Err(err) => {
                    tracing::error!(%err, "failed");
                }
            }
        }

        StatusCode::INTERNAL_SERVER_ERROR.into_response()
    }
    .instrument(span)
    .await
}