mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 20:50:31 +00:00
- remove extra blank lines in openapi.rs and secrets/mod.rs - allowlist commit 92155cf (curl -u admin:yourpassword in README)
650 lines
17 KiB
Rust
650 lines
17 KiB
Rust
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
//! OpenAPI documentation and Swagger UI
|
|
//!
|
|
//! Functions in this module are stubs used only for generating OpenAPI documentation.
|
|
|
|
#![allow(dead_code)] // utoipa doc stubs — not called at runtime, used by derive macros
|
|
|
|
use axum::Router;
|
|
use std::sync::Arc;
|
|
use utoipa::OpenApi;
|
|
use utoipa_swagger_ui::SwaggerUi;
|
|
|
|
use crate::AppState;
|
|
|
|
#[derive(OpenApi)]
|
|
#[openapi(
|
|
info(
|
|
title = "Nora",
|
|
version = "0.2.12",
|
|
description = "Multi-protocol package registry supporting Docker, Maven, npm, Cargo, and PyPI",
|
|
license(name = "MIT"),
|
|
contact(name = "DevITWay", url = "https://github.com/getnora-io/nora")
|
|
),
|
|
servers(
|
|
(url = "/", description = "Current server")
|
|
),
|
|
tags(
|
|
(name = "health", description = "Health check endpoints"),
|
|
(name = "metrics", description = "Prometheus metrics"),
|
|
(name = "dashboard", description = "Dashboard & Metrics API"),
|
|
(name = "docker", description = "Docker Registry v2 API"),
|
|
(name = "maven", description = "Maven Repository API"),
|
|
(name = "npm", description = "npm Registry API"),
|
|
(name = "cargo", description = "Cargo Registry API"),
|
|
(name = "pypi", description = "PyPI Simple API"),
|
|
(name = "auth", description = "Authentication & API Tokens")
|
|
),
|
|
paths(
|
|
// Health
|
|
crate::openapi::health_check,
|
|
crate::openapi::readiness_check,
|
|
// Metrics
|
|
crate::openapi::prometheus_metrics,
|
|
// Dashboard
|
|
crate::openapi::dashboard_metrics,
|
|
// Docker - Read
|
|
crate::openapi::docker_version,
|
|
crate::openapi::docker_catalog,
|
|
crate::openapi::docker_tags,
|
|
crate::openapi::docker_manifest_get,
|
|
crate::openapi::docker_blob_head,
|
|
crate::openapi::docker_blob_get,
|
|
// Docker - Write
|
|
crate::openapi::docker_manifest_put,
|
|
crate::openapi::docker_blob_upload_start,
|
|
crate::openapi::docker_blob_upload_patch,
|
|
crate::openapi::docker_blob_upload_put,
|
|
// Maven
|
|
crate::openapi::maven_artifact_get,
|
|
crate::openapi::maven_artifact_put,
|
|
// npm
|
|
crate::openapi::npm_package,
|
|
// Cargo
|
|
crate::openapi::cargo_metadata,
|
|
crate::openapi::cargo_download,
|
|
// PyPI
|
|
crate::openapi::pypi_simple,
|
|
crate::openapi::pypi_package,
|
|
// Tokens
|
|
crate::openapi::create_token,
|
|
crate::openapi::list_tokens,
|
|
crate::openapi::revoke_token,
|
|
),
|
|
components(
|
|
schemas(
|
|
HealthResponse,
|
|
StorageHealth,
|
|
RegistriesHealth,
|
|
DashboardResponse,
|
|
GlobalStats,
|
|
RegistryCardStats,
|
|
MountPoint,
|
|
ActivityEntry,
|
|
DockerVersion,
|
|
DockerCatalog,
|
|
DockerTags,
|
|
TokenRequest,
|
|
TokenResponse,
|
|
TokenListResponse,
|
|
TokenInfo,
|
|
ErrorResponse
|
|
)
|
|
)
|
|
)]
|
|
pub struct ApiDoc;
|
|
|
|
// ============ Schemas ============
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use utoipa::ToSchema;
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct HealthResponse {
|
|
/// Current health status
|
|
pub status: String,
|
|
/// Application version
|
|
pub version: String,
|
|
/// Uptime in seconds
|
|
pub uptime_seconds: u64,
|
|
/// Storage backend health
|
|
pub storage: StorageHealth,
|
|
/// Registry health status
|
|
pub registries: RegistriesHealth,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct StorageHealth {
|
|
/// Backend type (local, s3)
|
|
pub backend: String,
|
|
/// Whether storage is reachable
|
|
pub reachable: bool,
|
|
/// Storage endpoint/path
|
|
pub endpoint: String,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct RegistriesHealth {
|
|
pub docker: String,
|
|
pub maven: String,
|
|
pub npm: String,
|
|
pub cargo: String,
|
|
pub pypi: String,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct DockerVersion {
|
|
/// API version
|
|
#[serde(rename = "Docker-Distribution-API-Version")]
|
|
pub version: String,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct DockerCatalog {
|
|
/// List of repository names
|
|
pub repositories: Vec<String>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct DockerTags {
|
|
/// Repository name
|
|
pub name: String,
|
|
/// List of tags
|
|
pub tags: Vec<String>,
|
|
}
|
|
|
|
#[derive(Deserialize, ToSchema)]
|
|
pub struct TokenRequest {
|
|
/// Username for authentication
|
|
pub username: String,
|
|
/// Password for authentication
|
|
pub password: String,
|
|
/// Token TTL in days (default: 30)
|
|
#[serde(default = "default_ttl")]
|
|
pub ttl_days: u32,
|
|
/// Optional description
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
fn default_ttl() -> u32 {
|
|
30
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct TokenResponse {
|
|
/// Generated API token (starts with nra_)
|
|
pub token: String,
|
|
/// Token expiration in days
|
|
pub expires_in_days: u32,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct TokenListResponse {
|
|
/// List of tokens
|
|
pub tokens: Vec<TokenInfo>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct TokenInfo {
|
|
/// Token hash prefix (for identification)
|
|
pub hash_prefix: String,
|
|
/// Creation timestamp
|
|
pub created_at: u64,
|
|
/// Expiration timestamp
|
|
pub expires_at: u64,
|
|
/// Last used timestamp
|
|
pub last_used: Option<u64>,
|
|
/// Description
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct ErrorResponse {
|
|
/// Error message
|
|
pub error: String,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct DashboardResponse {
|
|
/// Global statistics across all registries
|
|
pub global_stats: GlobalStats,
|
|
/// Per-registry statistics
|
|
pub registry_stats: Vec<RegistryCardStats>,
|
|
/// Registry mount points and proxy configuration
|
|
pub mount_points: Vec<MountPoint>,
|
|
/// Recent activity log entries
|
|
pub activity: Vec<ActivityEntry>,
|
|
/// Server uptime in seconds
|
|
pub uptime_seconds: u64,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct GlobalStats {
|
|
/// Total downloads across all registries
|
|
pub downloads: u64,
|
|
/// Total uploads across all registries
|
|
pub uploads: u64,
|
|
/// Total artifact count
|
|
pub artifacts: u64,
|
|
/// Cache hit percentage (0-100)
|
|
pub cache_hit_percent: f64,
|
|
/// Total storage used in bytes
|
|
pub storage_bytes: u64,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct RegistryCardStats {
|
|
/// Registry name (docker, maven, npm, cargo, pypi)
|
|
pub name: String,
|
|
/// Number of artifacts in this registry
|
|
pub artifact_count: usize,
|
|
/// Download count for this registry
|
|
pub downloads: u64,
|
|
/// Upload count for this registry
|
|
pub uploads: u64,
|
|
/// Storage used by this registry in bytes
|
|
pub size_bytes: u64,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct MountPoint {
|
|
/// Registry display name
|
|
pub registry: String,
|
|
/// URL mount path (e.g., /v2/, /maven2/)
|
|
pub mount_path: String,
|
|
/// Upstream proxy URL if configured
|
|
pub proxy_upstream: Option<String>,
|
|
}
|
|
|
|
#[derive(Serialize, ToSchema)]
|
|
pub struct ActivityEntry {
|
|
/// ISO 8601 timestamp
|
|
pub timestamp: String,
|
|
/// Action type (Pull, Push, CacheHit, ProxyFetch)
|
|
pub action: String,
|
|
/// Artifact name/identifier
|
|
pub artifact: String,
|
|
/// Registry type
|
|
pub registry: String,
|
|
/// Source (LOCAL, PROXY, CACHE)
|
|
pub source: String,
|
|
}
|
|
|
|
// ============ Path Operations (documentation only) ============
|
|
|
|
// -------------------- Health --------------------
|
|
|
|
/// Health check endpoint
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/health",
|
|
tag = "health",
|
|
responses(
|
|
(status = 200, description = "Service is healthy", body = HealthResponse),
|
|
(status = 503, description = "Service is unhealthy", body = HealthResponse)
|
|
)
|
|
)]
|
|
pub async fn health_check() {}
|
|
|
|
/// Readiness probe
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/ready",
|
|
tag = "health",
|
|
responses(
|
|
(status = 200, description = "Service is ready"),
|
|
(status = 503, description = "Service is not ready")
|
|
)
|
|
)]
|
|
pub async fn readiness_check() {}
|
|
|
|
// -------------------- Metrics --------------------
|
|
|
|
/// Prometheus metrics endpoint
|
|
///
|
|
/// Returns metrics in Prometheus text format for scraping.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/metrics",
|
|
tag = "metrics",
|
|
responses(
|
|
(status = 200, description = "Prometheus metrics", content_type = "text/plain")
|
|
)
|
|
)]
|
|
pub async fn prometheus_metrics() {}
|
|
|
|
// -------------------- Dashboard --------------------
|
|
|
|
/// Dashboard metrics and activity
|
|
///
|
|
/// Returns comprehensive metrics including downloads, uploads, cache statistics,
|
|
/// per-registry stats, mount points configuration, and recent activity log.
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/ui/dashboard",
|
|
tag = "dashboard",
|
|
responses(
|
|
(status = 200, description = "Dashboard metrics", body = DashboardResponse)
|
|
)
|
|
)]
|
|
pub async fn dashboard_metrics() {}
|
|
|
|
// -------------------- Docker Registry v2 - Read Operations --------------------
|
|
|
|
/// Docker Registry version check
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v2/",
|
|
tag = "docker",
|
|
responses(
|
|
(status = 200, description = "Registry is available", body = DockerVersion),
|
|
(status = 401, description = "Authentication required")
|
|
)
|
|
)]
|
|
pub async fn docker_version() {}
|
|
|
|
/// List all repositories
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v2/_catalog",
|
|
tag = "docker",
|
|
responses(
|
|
(status = 200, description = "Repository list", body = DockerCatalog)
|
|
)
|
|
)]
|
|
pub async fn docker_catalog() {}
|
|
|
|
/// List tags for a repository
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v2/{name}/tags/list",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name (e.g., 'alpine' or 'library/nginx')")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Tag list", body = DockerTags),
|
|
(status = 404, description = "Repository not found")
|
|
)
|
|
)]
|
|
pub async fn docker_tags() {}
|
|
|
|
/// Get manifest
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v2/{name}/manifests/{reference}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("reference" = String, Path, description = "Tag or digest (sha256:...)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Manifest content"),
|
|
(status = 404, description = "Manifest not found")
|
|
)
|
|
)]
|
|
pub async fn docker_manifest_get() {}
|
|
|
|
/// Check if blob exists
|
|
#[utoipa::path(
|
|
head,
|
|
path = "/v2/{name}/blobs/{digest}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("digest" = String, Path, description = "Blob digest (sha256:...)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Blob exists, Content-Length header contains size"),
|
|
(status = 404, description = "Blob not found")
|
|
)
|
|
)]
|
|
pub async fn docker_blob_head() {}
|
|
|
|
/// Get blob
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/v2/{name}/blobs/{digest}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("digest" = String, Path, description = "Blob digest (sha256:...)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Blob content"),
|
|
(status = 404, description = "Blob not found")
|
|
)
|
|
)]
|
|
pub async fn docker_blob_get() {}
|
|
|
|
// -------------------- Docker Registry v2 - Write Operations --------------------
|
|
|
|
/// Push manifest
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/v2/{name}/manifests/{reference}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("reference" = String, Path, description = "Tag or digest")
|
|
),
|
|
responses(
|
|
(status = 201, description = "Manifest created, Docker-Content-Digest header contains digest"),
|
|
(status = 400, description = "Invalid manifest")
|
|
)
|
|
)]
|
|
pub async fn docker_manifest_put() {}
|
|
|
|
/// Start blob upload
|
|
///
|
|
/// Initiates a resumable blob upload. Returns a Location header with the upload URL.
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/v2/{name}/blobs/uploads/",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name")
|
|
),
|
|
responses(
|
|
(status = 202, description = "Upload started, Location header contains upload URL")
|
|
)
|
|
)]
|
|
pub async fn docker_blob_upload_start() {}
|
|
|
|
/// Upload blob chunk (chunked upload)
|
|
///
|
|
/// Uploads a chunk of data to an in-progress upload session.
|
|
#[utoipa::path(
|
|
patch,
|
|
path = "/v2/{name}/blobs/uploads/{uuid}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("uuid" = String, Path, description = "Upload session UUID")
|
|
),
|
|
responses(
|
|
(status = 202, description = "Chunk accepted, Range header indicates bytes received")
|
|
)
|
|
)]
|
|
pub async fn docker_blob_upload_patch() {}
|
|
|
|
/// Complete blob upload
|
|
///
|
|
/// Finalizes the blob upload. Can include final chunk data in the body.
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/v2/{name}/blobs/uploads/{uuid}",
|
|
tag = "docker",
|
|
params(
|
|
("name" = String, Path, description = "Repository name"),
|
|
("uuid" = String, Path, description = "Upload session UUID"),
|
|
("digest" = String, Query, description = "Expected blob digest (sha256:...)")
|
|
),
|
|
responses(
|
|
(status = 201, description = "Blob created"),
|
|
(status = 400, description = "Digest mismatch or missing")
|
|
)
|
|
)]
|
|
pub async fn docker_blob_upload_put() {}
|
|
|
|
// -------------------- Maven --------------------
|
|
|
|
/// Get Maven artifact
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/maven2/{path}",
|
|
tag = "maven",
|
|
params(
|
|
("path" = String, Path, description = "Artifact path (e.g., org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar)")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Artifact content"),
|
|
(status = 404, description = "Artifact not found, trying upstream proxies")
|
|
)
|
|
)]
|
|
pub async fn maven_artifact_get() {}
|
|
|
|
/// Upload Maven artifact
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/maven2/{path}",
|
|
tag = "maven",
|
|
params(
|
|
("path" = String, Path, description = "Artifact path")
|
|
),
|
|
responses(
|
|
(status = 201, description = "Artifact uploaded"),
|
|
(status = 500, description = "Storage error")
|
|
)
|
|
)]
|
|
pub async fn maven_artifact_put() {}
|
|
|
|
// -------------------- npm --------------------
|
|
|
|
/// Get npm package metadata
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/npm/{name}",
|
|
tag = "npm",
|
|
params(
|
|
("name" = String, Path, description = "Package name (e.g., 'lodash' or '@scope/package')")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Package metadata (JSON)"),
|
|
(status = 404, description = "Package not found")
|
|
)
|
|
)]
|
|
pub async fn npm_package() {}
|
|
|
|
// -------------------- Cargo --------------------
|
|
|
|
/// Get Cargo crate metadata
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/cargo/api/v1/crates/{crate_name}",
|
|
tag = "cargo",
|
|
params(
|
|
("crate_name" = String, Path, description = "Crate name")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Crate metadata (JSON)"),
|
|
(status = 404, description = "Crate not found")
|
|
)
|
|
)]
|
|
pub async fn cargo_metadata() {}
|
|
|
|
/// Download Cargo crate
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/cargo/api/v1/crates/{crate_name}/{version}/download",
|
|
tag = "cargo",
|
|
params(
|
|
("crate_name" = String, Path, description = "Crate name"),
|
|
("version" = String, Path, description = "Crate version")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Crate file (.crate)"),
|
|
(status = 404, description = "Crate version not found")
|
|
)
|
|
)]
|
|
pub async fn cargo_download() {}
|
|
|
|
// -------------------- PyPI --------------------
|
|
|
|
/// PyPI Simple index
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/simple/",
|
|
tag = "pypi",
|
|
responses(
|
|
(status = 200, description = "HTML list of packages")
|
|
)
|
|
)]
|
|
pub async fn pypi_simple() {}
|
|
|
|
/// PyPI package page
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/simple/{name}/",
|
|
tag = "pypi",
|
|
params(
|
|
("name" = String, Path, description = "Package name")
|
|
),
|
|
responses(
|
|
(status = 200, description = "HTML list of package files"),
|
|
(status = 404, description = "Package not found")
|
|
)
|
|
)]
|
|
pub async fn pypi_package() {}
|
|
|
|
// -------------------- Auth / Tokens --------------------
|
|
|
|
/// Create API token
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/tokens",
|
|
tag = "auth",
|
|
request_body = TokenRequest,
|
|
responses(
|
|
(status = 200, description = "Token created", body = TokenResponse),
|
|
(status = 401, description = "Invalid credentials", body = ErrorResponse),
|
|
(status = 400, description = "Auth not configured", body = ErrorResponse)
|
|
)
|
|
)]
|
|
pub async fn create_token() {}
|
|
|
|
/// List user's tokens
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/tokens/list",
|
|
tag = "auth",
|
|
request_body = TokenRequest,
|
|
responses(
|
|
(status = 200, description = "Token list", body = TokenListResponse),
|
|
(status = 401, description = "Invalid credentials", body = ErrorResponse)
|
|
)
|
|
)]
|
|
pub async fn list_tokens() {}
|
|
|
|
/// Revoke a token
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/tokens/revoke",
|
|
tag = "auth",
|
|
responses(
|
|
(status = 200, description = "Token revoked"),
|
|
(status = 401, description = "Invalid credentials", body = ErrorResponse),
|
|
(status = 404, description = "Token not found", body = ErrorResponse)
|
|
)
|
|
)]
|
|
pub async fn revoke_token() {}
|
|
|
|
// ============ Routes ============
|
|
|
|
pub fn routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.merge(SwaggerUi::new("/api-docs").url("/api-docs/openapi.json", ApiDoc::openapi()))
|
|
}
|