//! Internal API endpoints for service-to-service communication. //! //! Most endpoints are protected by `ServiceAuth`, a shared-secret bearer token, //! and are called by the CLI SSH server running on the same host. Two are not: //! `/api/internal/alerts` and `/api/internal/synckit/client-versions` take //! `AlertsAuth`, a separate bearer token carried by PoM and MT. pub(crate) mod alerts; mod cli_features; mod content; mod creators; mod git; mod items; mod repos; mod synckit; mod uploads; pub(super) use git::restart_status; use axum::routing::get; use crate::{ AppState, csrf::{CsrfRouter, delete_csrf_skip, post_csrf_skip, put_csrf_skip, with_csrf_skip}, }; /// Routes carrying this reason are authed by `ServiceAuth`, a shared-secret /// bearer token (no session), so CSRF is not applicable. The two `AlertsAuth` /// routes carry `ALERTS_SKIP` instead. Each registration names its reason so /// the posture is visible at the call site. const INTERNAL_SKIP: &str = "internal API: shared-secret bearer (ServiceAuth), no session"; /// Internal service-to-service routes (ServiceAuth, no rate limit). pub(super) fn internal_routes() -> CsrfRouter { CsrfRouter::new() .route_get("/api/internal/ssh-key-lookup", get(git::ssh_key_lookup)) .route_get( "/api/internal/synckit/client-versions", get(synckit::client_versions), ) .route( "/api/internal/creator/projects", with_csrf_skip( INTERNAL_SKIP, get(creators::creator_projects).post(cli_features::create_project), ), ) .route_get( "/api/internal/creator/projects/{id}/items", get(creators::creator_project_items), ) .route_get("/api/internal/creator/stats", get(creators::creator_stats)) // Git repo + SSH key management for the CLI. Keyed by repo NAME, not // id: the caller is a person at a terminal who has the name and would // otherwise have to look a UUID up first. See internal/repos.rs. .route_get("/api/internal/creator/repos", get(repos::repo_list)) .route( "/api/internal/creator/repos/{name}", with_csrf_skip( INTERNAL_SKIP, get(repos::repo_info).delete(repos::repo_delete), ), ) .route( "/api/internal/creator/repos/{name}/visibility", put_csrf_skip(INTERNAL_SKIP, repos::repo_set_visibility), ) .route( "/api/internal/creator/repos/{name}/description", put_csrf_skip(INTERNAL_SKIP, repos::repo_set_description), ) .route( "/api/internal/creator/ssh-keys/{fingerprint}", delete_csrf_skip(INTERNAL_SKIP, repos::key_remove), ) .route( "/api/internal/creator/items", post_csrf_skip(INTERNAL_SKIP, items::create_item), ) .route( "/api/internal/upload/presign", post_csrf_skip(INTERNAL_SKIP, uploads::presign_upload), ) .route( "/api/internal/upload/confirm", post_csrf_skip(INTERNAL_SKIP, uploads::confirm_upload), ) .route( "/api/internal/upload/multipart/start", post_csrf_skip(INTERNAL_SKIP, uploads::multipart_start), ) .route( "/api/internal/upload/multipart/parts", post_csrf_skip(INTERNAL_SKIP, uploads::multipart_parts), ) .route( "/api/internal/upload/multipart/complete", post_csrf_skip(INTERNAL_SKIP, uploads::multipart_complete), ) .route( "/api/internal/upload/multipart/abort", post_csrf_skip(INTERNAL_SKIP, uploads::multipart_abort), ) .route_get( "/api/internal/creator/storage", get(uploads::creator_storage), ) .route_get("/api/internal/creator/items/{id}", get(items::get_item)) .route( "/api/internal/creator/items/{id}", put_csrf_skip(INTERNAL_SKIP, items::update_item), ) .route( "/api/internal/creator/items/{id}", delete_csrf_skip(INTERNAL_SKIP, items::delete_item), ) .route( "/api/internal/creator/items/{id}/publish", post_csrf_skip(INTERNAL_SKIP, items::publish_item), ) .route( "/api/internal/creator/items/{id}/unpublish", post_csrf_skip(INTERNAL_SKIP, items::unpublish_item), ) .route_get( "/api/internal/creator/items/{id}/versions", get(items::item_versions), ) // Blog posts .route_get( "/api/internal/creator/projects/{id}/blog", get(content::list_blog_posts), ) .route( "/api/internal/creator/blog", post_csrf_skip(INTERNAL_SKIP, content::create_blog_post), ) .route( "/api/internal/creator/blog/{id}", delete_csrf_skip(INTERNAL_SKIP, content::delete_blog_post), ) // Promo codes .route( "/api/internal/creator/promo-codes", with_csrf_skip( INTERNAL_SKIP, get(content::list_promo_codes).post(content::create_promo_code), ), ) .route( "/api/internal/creator/promo-codes/{id}", delete_csrf_skip(INTERNAL_SKIP, content::delete_promo_code), ) // License keys .route( "/api/internal/creator/items/{id}/keys", with_csrf_skip( INTERNAL_SKIP, get(content::list_license_keys).post(content::generate_license_key), ), ) .route( "/api/internal/creator/keys/{id}/revoke", post_csrf_skip(INTERNAL_SKIP, content::revoke_license_key), ) // Analytics + export .route_get( "/api/internal/creator/analytics", get(creators::creator_analytics), ) .route_get( "/api/internal/creator/transactions", get(creators::creator_transactions), ) .route_get( "/api/internal/creator/export/sales", get(creators::export_sales), ) // Settings .route_get("/api/internal/creator/ssh-keys", get(git::list_ssh_keys)) // Git authorization .route( "/api/internal/git/authorize", post_csrf_skip(INTERNAL_SKIP, git::git_authorize), ) .route( "/api/internal/restart-warning", post_csrf_skip(INTERNAL_SKIP, git::set_restart_warning), ) // CLI features: tags .route_get( "/api/internal/creator/items/{id}/tags", get(cli_features::list_item_tags), ) .route( "/api/internal/creator/items/tags", post_csrf_skip(INTERNAL_SKIP, cli_features::add_item_tag), ) .route( "/api/internal/creator/items/tags/remove", post_csrf_skip(INTERNAL_SKIP, cli_features::remove_item_tag), ) .route_get("/api/internal/tags/search", get(cli_features::search_tags)) // CLI features: broadcast .route( "/api/internal/creator/broadcast", post_csrf_skip(INTERNAL_SKIP, cli_features::send_broadcast), ) // CLI features: tiers .route_get( "/api/internal/creator/projects/{id}/tiers", get(cli_features::list_tiers), ) // CLI features: collections .route( "/api/internal/creator/collections", with_csrf_skip( INTERNAL_SKIP, get(cli_features::list_collections).post(cli_features::create_collection), ), ) .route( "/api/internal/creator/collections/{id}", delete_csrf_skip(INTERNAL_SKIP, cli_features::delete_collection), ) // CLI features: custom domains .route( "/api/internal/creator/domain", with_csrf_skip( INTERNAL_SKIP, get(cli_features::get_domain) .post(cli_features::add_domain) .delete(cli_features::remove_domain), ), ) .route( "/api/internal/creator/domain/verify", post_csrf_skip(INTERNAL_SKIP, cli_features::verify_domain), ) // Inbound infra alerts (PoM/MT). Bearer-authed via `AlertsAuth` on a // dedicated token, not `ServiceAuth`; no session, so CSRF is skipped. .route( "/api/internal/alerts", post_csrf_skip(ALERTS_SKIP, alerts::ingest_alert), ) } /// The alerts ingestion route is bearer-authed via `AlertsAuth` (dedicated /// token, no session); CSRF is not applicable. const ALERTS_SKIP: &str = "internal alerts: AlertsAuth bearer, no session";