feat(project): init
This commit is contained in:
81
src/api/docs.rs
Normal file
81
src/api/docs.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
|
||||
use utoipa::{Modify, OpenApi};
|
||||
|
||||
struct SecurityAddon;
|
||||
|
||||
impl Modify for SecurityAddon {
|
||||
fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
|
||||
let components = openapi
|
||||
.components
|
||||
.get_or_insert_with(utoipa::openapi::Components::new);
|
||||
components.add_security_scheme(
|
||||
"bearer_auth",
|
||||
SecurityScheme::Http(
|
||||
HttpBuilder::new()
|
||||
.scheme(HttpAuthScheme::Bearer)
|
||||
.bearer_format("JWT")
|
||||
.build(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
modifiers(&SecurityAddon),
|
||||
info(
|
||||
title = "CMS Service API",
|
||||
version = "0.1.0",
|
||||
description = include_str!("../../docs/API.md")
|
||||
),
|
||||
paths(
|
||||
crate::api::handlers::column::create_column_handler,
|
||||
crate::api::handlers::column::list_columns_handler,
|
||||
crate::api::handlers::column::get_column_handler,
|
||||
crate::api::handlers::column::update_column_handler,
|
||||
crate::api::handlers::column::delete_column_handler,
|
||||
crate::api::handlers::tag::create_tag_handler,
|
||||
crate::api::handlers::tag::list_tags_handler,
|
||||
crate::api::handlers::tag::get_tag_handler,
|
||||
crate::api::handlers::tag::update_tag_handler,
|
||||
crate::api::handlers::tag::delete_tag_handler,
|
||||
crate::api::handlers::media::create_media_handler,
|
||||
crate::api::handlers::media::list_media_handler,
|
||||
crate::api::handlers::media::get_media_handler,
|
||||
crate::api::handlers::media::delete_media_handler,
|
||||
crate::api::handlers::article::create_article_handler,
|
||||
crate::api::handlers::article::list_articles_handler,
|
||||
crate::api::handlers::article::get_article_handler,
|
||||
crate::api::handlers::article::update_article_handler,
|
||||
crate::api::handlers::article::publish_article_handler,
|
||||
crate::api::handlers::article::rollback_article_handler,
|
||||
crate::api::handlers::article::list_versions_handler
|
||||
),
|
||||
components(
|
||||
schemas(
|
||||
crate::api::handlers::column::CreateColumnRequest,
|
||||
crate::api::handlers::column::UpdateColumnRequest,
|
||||
crate::api::handlers::tag::CreateTagRequest,
|
||||
crate::api::handlers::tag::UpdateTagRequest,
|
||||
crate::api::handlers::media::CreateMediaRequest,
|
||||
crate::api::handlers::article::CreateArticleRequest,
|
||||
crate::api::handlers::article::UpdateArticleRequest,
|
||||
crate::api::handlers::article::RollbackRequest,
|
||||
crate::domain::models::Column,
|
||||
crate::domain::models::Tag,
|
||||
crate::domain::models::Media,
|
||||
crate::domain::models::Article,
|
||||
crate::domain::models::ArticleVersion,
|
||||
crate::infrastructure::repositories::article::ArticleWithTags
|
||||
)
|
||||
),
|
||||
tags(
|
||||
(name = "System", description = "系统:健康检查/文档"),
|
||||
(name = "Column", description = "栏目管理"),
|
||||
(name = "Article", description = "文章管理"),
|
||||
(name = "Media", description = "媒体库"),
|
||||
(name = "Tag", description = "标签与分类"),
|
||||
(name = "Version", description = "版本与回滚")
|
||||
)
|
||||
)]
|
||||
pub struct ApiDoc;
|
||||
334
src/api/handlers/article.rs
Normal file
334
src/api/handlers/article.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
routing::{get, post},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::{AppState, handlers::common::extract_bearer_token};
|
||||
use auth_kit::middleware::{tenant::TenantId, auth::AuthContext};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateArticleRequest {
|
||||
pub column_id: Option<Uuid>,
|
||||
pub title: String,
|
||||
pub slug: String,
|
||||
pub summary: Option<String>,
|
||||
pub content: String,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateArticleRequest {
|
||||
pub column_id: Option<Option<Uuid>>,
|
||||
pub title: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub summary: Option<Option<String>>,
|
||||
pub content: Option<String>,
|
||||
pub tag_ids: Option<Vec<Uuid>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, IntoParams)]
|
||||
pub struct ListArticlesQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
pub q: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub column_id: Option<Uuid>,
|
||||
pub tag_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct RollbackRequest {
|
||||
pub to_version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, IntoParams)]
|
||||
pub struct ListVersionsQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(create_article_handler).get(list_articles_handler))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(get_article_handler).patch(update_article_handler),
|
||||
)
|
||||
.route("/{id}/publish", post(publish_article_handler))
|
||||
.route("/{id}/rollback", post(rollback_article_handler))
|
||||
.route("/{id}/versions", get(list_versions_handler))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/articles",
|
||||
tag = "Article",
|
||||
request_body = CreateArticleRequest,
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "创建文章(草稿)", body = crate::infrastructure::repositories::article::ArticleWithTags)
|
||||
)
|
||||
)]
|
||||
pub async fn create_article_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<CreateArticleRequest>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::article::ArticleWithTags>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:write", &token)
|
||||
.await?;
|
||||
|
||||
let article = state
|
||||
.services
|
||||
.create_article(
|
||||
tenant_id,
|
||||
body.column_id,
|
||||
body.title,
|
||||
body.slug,
|
||||
body.summary,
|
||||
body.content,
|
||||
body.tag_ids.unwrap_or_default(),
|
||||
Some(user_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(article))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/articles",
|
||||
tag = "Article",
|
||||
params(ListArticlesQuery),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "文章列表/搜索", body = crate::infrastructure::repositories::column::Paged<crate::domain::models::Article>)
|
||||
)
|
||||
)]
|
||||
pub async fn list_articles_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(query): Query<ListArticlesQuery>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::column::Paged<crate::domain::models::Article>>, AppError>
|
||||
{
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", &token)
|
||||
.await?;
|
||||
|
||||
let result = state
|
||||
.services
|
||||
.list_articles(
|
||||
tenant_id,
|
||||
crate::infrastructure::repositories::article::ListArticlesQuery {
|
||||
page: query.page.unwrap_or(1),
|
||||
page_size: query.page_size.unwrap_or(20),
|
||||
q: query.q,
|
||||
status: query.status,
|
||||
column_id: query.column_id,
|
||||
tag_id: query.tag_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/articles/{id}",
|
||||
tag = "Article",
|
||||
params(
|
||||
("id" = String, Path, description = "文章ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "文章详情", body = crate::infrastructure::repositories::article::ArticleWithTags)
|
||||
)
|
||||
)]
|
||||
pub async fn get_article_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::article::ArticleWithTags>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", &token)
|
||||
.await?;
|
||||
|
||||
let article = state.services.get_article(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(article))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/articles/{id}",
|
||||
tag = "Article",
|
||||
request_body = UpdateArticleRequest,
|
||||
params(
|
||||
("id" = String, Path, description = "文章ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "更新文章", body = crate::infrastructure::repositories::article::ArticleWithTags)
|
||||
)
|
||||
)]
|
||||
pub async fn update_article_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateArticleRequest>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::article::ArticleWithTags>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:write", &token)
|
||||
.await?;
|
||||
|
||||
let article = state
|
||||
.services
|
||||
.update_article(
|
||||
tenant_id,
|
||||
id,
|
||||
body.column_id,
|
||||
body.title,
|
||||
body.slug,
|
||||
body.summary,
|
||||
body.content,
|
||||
body.tag_ids,
|
||||
Some(user_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(article))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/articles/{id}/publish",
|
||||
tag = "Article",
|
||||
params(
|
||||
("id" = String, Path, description = "文章ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "发布文章", body = crate::domain::models::Article)
|
||||
)
|
||||
)]
|
||||
pub async fn publish_article_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<crate::domain::models::Article>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:publish", &token)
|
||||
.await?;
|
||||
|
||||
let article = state.services.publish_article(tenant_id, id, Some(user_id)).await?;
|
||||
Ok(AppResponse::ok(article))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/articles/{id}/rollback",
|
||||
tag = "Version",
|
||||
request_body = RollbackRequest,
|
||||
params(
|
||||
("id" = String, Path, description = "文章ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "回滚到指定版本并生成新版本", body = crate::domain::models::Article)
|
||||
)
|
||||
)]
|
||||
pub async fn rollback_article_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<RollbackRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Article>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:rollback", &token)
|
||||
.await?;
|
||||
|
||||
let article = state
|
||||
.services
|
||||
.rollback_article(tenant_id, id, body.to_version, Some(user_id))
|
||||
.await?;
|
||||
Ok(AppResponse::ok(article))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/articles/{id}/versions",
|
||||
tag = "Version",
|
||||
params(
|
||||
("id" = String, Path, description = "文章ID"),
|
||||
ListVersionsQuery
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "版本列表", body = crate::infrastructure::repositories::column::Paged<crate::domain::models::ArticleVersion>)
|
||||
)
|
||||
)]
|
||||
pub async fn list_versions_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Query(query): Query<ListVersionsQuery>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::column::Paged<crate::domain::models::ArticleVersion>>, AppError>
|
||||
{
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:article:read", &token)
|
||||
.await?;
|
||||
|
||||
let versions = state
|
||||
.services
|
||||
.list_versions(
|
||||
tenant_id,
|
||||
id,
|
||||
query.page.unwrap_or(1),
|
||||
query.page_size.unwrap_or(20),
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(versions))
|
||||
}
|
||||
247
src/api/handlers/column.rs
Normal file
247
src/api/handlers/column.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
routing::{get, post},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::{AppState, handlers::common::extract_bearer_token};
|
||||
use auth_kit::middleware::{tenant::TenantId, auth::AuthContext};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateColumnRequest {
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
pub description: Option<String>,
|
||||
pub parent_id: Option<Uuid>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateColumnRequest {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub description: Option<Option<String>>,
|
||||
pub parent_id: Option<Option<Uuid>>,
|
||||
pub sort_order: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, IntoParams)]
|
||||
pub struct ListColumnsQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
pub search: Option<String>,
|
||||
pub parent_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(create_column_handler).get(list_columns_handler))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(get_column_handler)
|
||||
.patch(update_column_handler)
|
||||
.delete(delete_column_handler),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/columns",
|
||||
tag = "Column",
|
||||
request_body = CreateColumnRequest,
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "创建栏目", body = crate::domain::models::Column),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
)
|
||||
)]
|
||||
pub async fn create_column_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<CreateColumnRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Column>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:column:write", &token)
|
||||
.await?;
|
||||
|
||||
let column = state
|
||||
.services
|
||||
.create_column(
|
||||
tenant_id,
|
||||
body.name,
|
||||
body.slug,
|
||||
body.description,
|
||||
body.parent_id,
|
||||
body.sort_order.unwrap_or(0),
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(column))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/columns",
|
||||
tag = "Column",
|
||||
params(ListColumnsQuery),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "栏目列表", body = crate::infrastructure::repositories::column::Paged<crate::domain::models::Column>),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限")
|
||||
)
|
||||
)]
|
||||
pub async fn list_columns_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(query): Query<ListColumnsQuery>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::column::Paged<crate::domain::models::Column>>, AppError>
|
||||
{
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:column:read", &token)
|
||||
.await?;
|
||||
|
||||
let result = state
|
||||
.services
|
||||
.list_columns(
|
||||
tenant_id,
|
||||
crate::infrastructure::repositories::column::ListColumnsQuery {
|
||||
page: query.page.unwrap_or(1),
|
||||
page_size: query.page_size.unwrap_or(20),
|
||||
search: query.search,
|
||||
parent_id: query.parent_id,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/columns/{id}",
|
||||
tag = "Column",
|
||||
params(
|
||||
("id" = String, Path, description = "栏目ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "栏目详情", body = crate::domain::models::Column),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "不存在")
|
||||
)
|
||||
)]
|
||||
pub async fn get_column_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<crate::domain::models::Column>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:column:read", &token)
|
||||
.await?;
|
||||
|
||||
let column = state.services.get_column(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(column))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/columns/{id}",
|
||||
tag = "Column",
|
||||
request_body = UpdateColumnRequest,
|
||||
params(
|
||||
("id" = String, Path, description = "栏目ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "更新栏目", body = crate::domain::models::Column),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "不存在")
|
||||
)
|
||||
)]
|
||||
pub async fn update_column_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateColumnRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Column>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:column:write", &token)
|
||||
.await?;
|
||||
|
||||
let column = state
|
||||
.services
|
||||
.update_column(
|
||||
tenant_id,
|
||||
id,
|
||||
body.name,
|
||||
body.slug,
|
||||
body.description,
|
||||
body.parent_id,
|
||||
body.sort_order,
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(column))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/columns/{id}",
|
||||
tag = "Column",
|
||||
params(
|
||||
("id" = String, Path, description = "栏目ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "删除成功"),
|
||||
(status = 401, description = "未认证"),
|
||||
(status = 403, description = "无权限"),
|
||||
(status = 404, description = "不存在")
|
||||
)
|
||||
)]
|
||||
pub async fn delete_column_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:column:write", &token)
|
||||
.await?;
|
||||
|
||||
state.services.delete_column(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({"deleted": true})))
|
||||
}
|
||||
11
src/api/handlers/common.rs
Normal file
11
src/api/handlers/common.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use axum::http::HeaderMap;
|
||||
use common_telemetry::AppError;
|
||||
|
||||
pub fn extract_bearer_token(headers: &HeaderMap) -> Result<String, AppError> {
|
||||
let token = headers
|
||||
.get(axum::http::header::AUTHORIZATION)
|
||||
.and_then(|h| h.to_str().ok())
|
||||
.and_then(|v| v.strip_prefix("Bearer "))
|
||||
.ok_or(AppError::MissingAuthHeader)?;
|
||||
Ok(token.to_string())
|
||||
}
|
||||
175
src/api/handlers/media.rs
Normal file
175
src/api/handlers/media.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
routing::{get, post},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::{AppState, handlers::common::extract_bearer_token};
|
||||
use auth_kit::middleware::{tenant::TenantId, auth::AuthContext};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateMediaRequest {
|
||||
pub url: String,
|
||||
pub mime_type: Option<String>,
|
||||
pub size_bytes: Option<i64>,
|
||||
pub width: Option<i32>,
|
||||
pub height: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, IntoParams)]
|
||||
pub struct ListMediaQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
pub search: Option<String>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(create_media_handler).get(list_media_handler))
|
||||
.route("/{id}", get(get_media_handler).delete(delete_media_handler))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/media",
|
||||
tag = "Media",
|
||||
request_body = CreateMediaRequest,
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "创建媒体记录", body = crate::domain::models::Media)
|
||||
)
|
||||
)]
|
||||
pub async fn create_media_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<CreateMediaRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Media>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:media:manage", &token)
|
||||
.await?;
|
||||
|
||||
let media = state
|
||||
.services
|
||||
.create_media(
|
||||
tenant_id,
|
||||
body.url,
|
||||
body.mime_type,
|
||||
body.size_bytes,
|
||||
body.width,
|
||||
body.height,
|
||||
Some(user_id),
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(media))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/media",
|
||||
tag = "Media",
|
||||
params(ListMediaQuery),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "媒体列表", body = crate::infrastructure::repositories::column::Paged<crate::domain::models::Media>)
|
||||
)
|
||||
)]
|
||||
pub async fn list_media_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(query): Query<ListMediaQuery>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::column::Paged<crate::domain::models::Media>>, AppError>
|
||||
{
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:media:read", &token)
|
||||
.await?;
|
||||
|
||||
let result = state
|
||||
.services
|
||||
.list_media(
|
||||
tenant_id,
|
||||
crate::infrastructure::repositories::media::ListMediaQuery {
|
||||
page: query.page.unwrap_or(1),
|
||||
page_size: query.page_size.unwrap_or(20),
|
||||
search: query.search,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/media/{id}",
|
||||
tag = "Media",
|
||||
params(
|
||||
("id" = String, Path, description = "媒体ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "媒体详情", body = crate::domain::models::Media)
|
||||
)
|
||||
)]
|
||||
pub async fn get_media_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<crate::domain::models::Media>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:media:read", &token)
|
||||
.await?;
|
||||
|
||||
let media = state.services.get_media(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(media))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/media/{id}",
|
||||
tag = "Media",
|
||||
params(
|
||||
("id" = String, Path, description = "媒体ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "删除成功")
|
||||
)
|
||||
)]
|
||||
pub async fn delete_media_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:media:manage", &token)
|
||||
.await?;
|
||||
|
||||
state.services.delete_media(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({"deleted": true})))
|
||||
}
|
||||
5
src/api/handlers/mod.rs
Normal file
5
src/api/handlers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod article;
|
||||
pub mod column;
|
||||
pub mod common;
|
||||
pub mod media;
|
||||
pub mod tag;
|
||||
214
src/api/handlers/tag.rs
Normal file
214
src/api/handlers/tag.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{Path, Query, State},
|
||||
routing::{get, post},
|
||||
};
|
||||
use common_telemetry::{AppError, AppResponse};
|
||||
use utoipa::IntoParams;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::{AppState, handlers::common::extract_bearer_token};
|
||||
use auth_kit::middleware::{tenant::TenantId, auth::AuthContext};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct CreateTagRequest {
|
||||
pub kind: String,
|
||||
pub name: String,
|
||||
pub slug: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct UpdateTagRequest {
|
||||
pub name: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, IntoParams)]
|
||||
pub struct ListTagsQuery {
|
||||
pub page: Option<u32>,
|
||||
pub page_size: Option<u32>,
|
||||
pub search: Option<String>,
|
||||
pub kind: Option<String>,
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", post(create_tag_handler).get(list_tags_handler))
|
||||
.route(
|
||||
"/{id}",
|
||||
get(get_tag_handler)
|
||||
.patch(update_tag_handler)
|
||||
.delete(delete_tag_handler),
|
||||
)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/v1/tags",
|
||||
tag = "Tag",
|
||||
request_body = CreateTagRequest,
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "创建标签/分类", body = crate::domain::models::Tag)
|
||||
)
|
||||
)]
|
||||
pub async fn create_tag_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(body): Json<CreateTagRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Tag>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:tag:write", &token)
|
||||
.await?;
|
||||
|
||||
let tag = state
|
||||
.services
|
||||
.create_tag(tenant_id, body.kind, body.name, body.slug)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(tag))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/tags",
|
||||
tag = "Tag",
|
||||
params(ListTagsQuery),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "标签/分类列表", body = crate::infrastructure::repositories::column::Paged<crate::domain::models::Tag>)
|
||||
)
|
||||
)]
|
||||
pub async fn list_tags_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Query(query): Query<ListTagsQuery>,
|
||||
) -> Result<AppResponse<crate::infrastructure::repositories::column::Paged<crate::domain::models::Tag>>, AppError>
|
||||
{
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:tag:read", &token)
|
||||
.await?;
|
||||
|
||||
let result = state
|
||||
.services
|
||||
.list_tags(
|
||||
tenant_id,
|
||||
crate::infrastructure::repositories::tag::ListTagsQuery {
|
||||
page: query.page.unwrap_or(1),
|
||||
page_size: query.page_size.unwrap_or(20),
|
||||
search: query.search,
|
||||
kind: query.kind,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(result))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/tags/{id}",
|
||||
tag = "Tag",
|
||||
params(
|
||||
("id" = String, Path, description = "标签/分类ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "详情", body = crate::domain::models::Tag)
|
||||
)
|
||||
)]
|
||||
pub async fn get_tag_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<crate::domain::models::Tag>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:tag:read", &token)
|
||||
.await?;
|
||||
|
||||
let tag = state.services.get_tag(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(tag))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/v1/tags/{id}",
|
||||
tag = "Tag",
|
||||
request_body = UpdateTagRequest,
|
||||
params(
|
||||
("id" = String, Path, description = "标签/分类ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "更新", body = crate::domain::models::Tag)
|
||||
)
|
||||
)]
|
||||
pub async fn update_tag_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<UpdateTagRequest>,
|
||||
) -> Result<AppResponse<crate::domain::models::Tag>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:tag:write", &token)
|
||||
.await?;
|
||||
|
||||
let tag = state
|
||||
.services
|
||||
.update_tag(tenant_id, id, body.name, body.slug)
|
||||
.await?;
|
||||
Ok(AppResponse::ok(tag))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/v1/tags/{id}",
|
||||
tag = "Tag",
|
||||
params(
|
||||
("id" = String, Path, description = "标签/分类ID")
|
||||
),
|
||||
security(
|
||||
("bearer_auth" = [])
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "删除成功")
|
||||
)
|
||||
)]
|
||||
pub async fn delete_tag_handler(
|
||||
TenantId(tenant_id): TenantId,
|
||||
AuthContext { user_id, .. }: AuthContext,
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<AppResponse<serde_json::Value>, AppError> {
|
||||
let token = extract_bearer_token(&headers)?;
|
||||
state
|
||||
.iam_client
|
||||
.require_permission(tenant_id, user_id, "cms:tag:write", &token)
|
||||
.await?;
|
||||
|
||||
state.services.delete_tag(tenant_id, id).await?;
|
||||
Ok(AppResponse::ok(serde_json::json!({"deleted": true})))
|
||||
}
|
||||
128
src/api/middleware/mod.rs
Normal file
128
src/api/middleware/mod.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use axum::{
|
||||
extract::{MatchedPath, Request},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use common_telemetry::AppError;
|
||||
use futures_util::FutureExt;
|
||||
use http::HeaderValue;
|
||||
use std::{panic::AssertUnwindSafe, time::Instant};
|
||||
|
||||
pub async fn ensure_request_id(mut req: Request, next: Next) -> Response {
|
||||
let request_id = req
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
|
||||
if let Ok(v) = HeaderValue::from_str(&request_id) {
|
||||
req.headers_mut().insert("x-request-id", v);
|
||||
}
|
||||
|
||||
let mut resp = next.run(req).await;
|
||||
if let Ok(v) = HeaderValue::from_str(&request_id) {
|
||||
resp.headers_mut().insert("x-request-id", v);
|
||||
}
|
||||
resp
|
||||
}
|
||||
|
||||
pub async fn request_logger(req: Request, next: Next) -> Response {
|
||||
let started = Instant::now();
|
||||
let method = req.method().to_string();
|
||||
let path = req.uri().path().to_string();
|
||||
let request_id = req
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let action = req
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(|m| format!("{} {}", method, m.as_str()))
|
||||
.unwrap_or_else(|| format!("{} {}", method, path));
|
||||
|
||||
let tenant_id = req
|
||||
.extensions()
|
||||
.get::<auth_kit::middleware::tenant::TenantId>()
|
||||
.map(|t| t.0.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let user_id = req
|
||||
.extensions()
|
||||
.get::<auth_kit::middleware::auth::AuthContext>()
|
||||
.map(|c| c.user_id.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let resp = next.run(req).await;
|
||||
let latency_ms = started.elapsed().as_millis() as u64;
|
||||
let status = resp.status().as_u16();
|
||||
|
||||
let error_code = match status {
|
||||
200..=399 => "ok",
|
||||
400 => "bad_request",
|
||||
401 => "unauthorized",
|
||||
403 => "permission_denied",
|
||||
404 => "not_found",
|
||||
409 => "conflict",
|
||||
429 => "rate_limited",
|
||||
500..=599 => "server_error",
|
||||
_ => "unknown",
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
trace_id = %request_id,
|
||||
tenant_id = %tenant_id,
|
||||
user_id = %user_id,
|
||||
action = %action,
|
||||
latency_ms = latency_ms,
|
||||
error_code = %error_code,
|
||||
status = status
|
||||
);
|
||||
|
||||
resp
|
||||
}
|
||||
|
||||
pub async fn catch_panic(req: Request, next: Next) -> Response {
|
||||
let request_id = req
|
||||
.headers()
|
||||
.get("x-request-id")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
let method = req.method().to_string();
|
||||
let path = req.uri().path().to_string();
|
||||
let action = req
|
||||
.extensions()
|
||||
.get::<MatchedPath>()
|
||||
.map(|m| format!("{} {}", method, m.as_str()))
|
||||
.unwrap_or_else(|| format!("{} {}", method, path));
|
||||
|
||||
let tenant_id = req
|
||||
.extensions()
|
||||
.get::<auth_kit::middleware::tenant::TenantId>()
|
||||
.map(|t| t.0.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let user_id = req
|
||||
.extensions()
|
||||
.get::<auth_kit::middleware::auth::AuthContext>()
|
||||
.map(|c| c.user_id.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let result = AssertUnwindSafe(next.run(req)).catch_unwind().await;
|
||||
match result {
|
||||
Ok(resp) => resp,
|
||||
Err(_) => {
|
||||
tracing::error!(
|
||||
trace_id = %request_id,
|
||||
tenant_id = %tenant_id,
|
||||
user_id = %user_id,
|
||||
action = %action,
|
||||
latency_ms = 0_u64,
|
||||
error_code = "panic"
|
||||
);
|
||||
AppError::AnyhowError(anyhow::anyhow!("panic")).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
40
src/api/mod.rs
Normal file
40
src/api/mod.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
pub mod docs;
|
||||
pub mod handlers;
|
||||
pub mod middleware;
|
||||
|
||||
use axum::routing::get;
|
||||
use axum::Router;
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_scalar::{Scalar, Servable};
|
||||
|
||||
use crate::api::docs::ApiDoc;
|
||||
use crate::api::middleware::{catch_panic, request_logger};
|
||||
use crate::application::services::CmsServices;
|
||||
use crate::infrastructure::iam_client::IamClient;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
pub services: CmsServices,
|
||||
pub iam_client: IamClient,
|
||||
}
|
||||
|
||||
pub fn build_router(state: AppState) -> Router {
|
||||
let health = Router::new().route("/healthz", get(|| async { axum::http::StatusCode::OK }));
|
||||
|
||||
let v1 = Router::new()
|
||||
.nest("/columns", handlers::column::router())
|
||||
.nest("/tags", handlers::tag::router())
|
||||
.nest("/media", handlers::media::router())
|
||||
.nest("/articles", handlers::article::router());
|
||||
|
||||
let app = Router::new()
|
||||
.route("/favicon.ico", get(|| async { axum::http::StatusCode::NO_CONTENT }))
|
||||
.merge(Scalar::with_url("/scalar", ApiDoc::openapi()))
|
||||
.merge(health)
|
||||
.nest("/v1", v1)
|
||||
.layer(axum::middleware::from_fn(catch_panic))
|
||||
.layer(axum::middleware::from_fn(request_logger))
|
||||
.with_state(state);
|
||||
|
||||
app
|
||||
}
|
||||
Reference in New Issue
Block a user