#![allow(dead_code)]
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;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, ToSchema)]
pub struct HealthResponse {
pub status: String,
pub version: String,
pub uptime_seconds: u64,
pub storage: StorageHealth,
pub registries: RegistriesHealth,
}
#[derive(Serialize, ToSchema)]
pub struct StorageHealth {
pub backend: String,
pub reachable: bool,
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 {
#[serde(rename = "Docker-Distribution-API-Version")]
pub version: String,
}
#[derive(Serialize, ToSchema)]
pub struct DockerCatalog {
pub repositories: Vec<String>,
}
#[derive(Serialize, ToSchema)]
pub struct DockerTags {
pub name: String,
pub tags: Vec<String>,
}
#[derive(Deserialize, ToSchema)]
pub struct TokenRequest {
pub username: String,
pub password: String,
#[serde(default = "default_ttl")]
pub ttl_days: u32,
pub description: Option<String>,
}
fn default_ttl() -> u32 {
30
}
#[derive(Serialize, ToSchema)]
pub struct TokenResponse {
pub token: String,
pub expires_in_days: u32,
}
#[derive(Serialize, ToSchema)]
pub struct TokenListResponse {
pub tokens: Vec<TokenInfo>,
}
#[derive(Serialize, ToSchema)]
pub struct TokenInfo {
pub hash_prefix: String,
pub created_at: u64,
pub expires_at: u64,
pub last_used: Option<u64>,
pub description: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ErrorResponse {
pub error: String,
}
#[derive(Serialize, ToSchema)]
pub struct DashboardResponse {
pub global_stats: GlobalStats,
pub registry_stats: Vec<RegistryCardStats>,
pub mount_points: Vec<MountPoint>,
pub activity: Vec<ActivityEntry>,
pub uptime_seconds: u64,
}
#[derive(Serialize, ToSchema)]
pub struct GlobalStats {
pub downloads: u64,
pub uploads: u64,
pub artifacts: u64,
pub cache_hit_percent: f64,
pub storage_bytes: u64,
}
#[derive(Serialize, ToSchema)]
pub struct RegistryCardStats {
pub name: String,
pub artifact_count: usize,
pub downloads: u64,
pub uploads: u64,
pub size_bytes: u64,
}
#[derive(Serialize, ToSchema)]
pub struct MountPoint {
pub registry: String,
pub mount_path: String,
pub proxy_upstream: Option<String>,
}
#[derive(Serialize, ToSchema)]
pub struct ActivityEntry {
pub timestamp: String,
pub action: String,
pub artifact: String,
pub registry: String,
pub source: String,
}
#[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() {}
#[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() {}
#[utoipa::path(
get,
path = "/metrics",
tag = "metrics",
responses(
(status = 200, description = "Prometheus metrics", content_type = "text/plain")
)
)]
pub async fn prometheus_metrics() {}
#[utoipa::path(
get,
path = "/api/ui/dashboard",
tag = "dashboard",
responses(
(status = 200, description = "Dashboard metrics", body = DashboardResponse)
)
)]
pub async fn dashboard_metrics() {}
#[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() {}
#[utoipa::path(
get,
path = "/v2/_catalog",
tag = "docker",
responses(
(status = 200, description = "Repository list", body = DockerCatalog)
)
)]
pub async fn docker_catalog() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
#[utoipa::path(
get,
path = "/simple/",
tag = "pypi",
responses(
(status = 200, description = "HTML list of packages")
)
)]
pub async fn pypi_simple() {}
#[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() {}
#[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() {}
#[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() {}
#[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() {}
pub fn routes() -> Router<Arc<AppState>> {
Router::new()
.merge(SwaggerUi::new("/api-docs").url("/api-docs/openapi.json", ApiDoc::openapi()))
}