feat(blog): 接入 posts 接口(CRUD + CORS PUT)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9d113b72f8
commit
edb975d818
@ -16,6 +16,7 @@ use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||
|
||||
use crate::folders::{FolderRegistry, WallState, QUOTA_BYTES, MAX_ITEMS};
|
||||
use crate::photos::{SaveError, MAX_BYTES, MAX_PHOTOS};
|
||||
use crate::posts::{Post, PostDraft, PostError, MAX_POSTS};
|
||||
use crate::routes::extract::AuthUser;
|
||||
|
||||
use crate::{
|
||||
@ -35,7 +36,7 @@ pub fn router(state: AppState) -> Router {
|
||||
"http://localhost:5174".parse().unwrap(),
|
||||
"http://127.0.0.1:5174".parse().unwrap(),
|
||||
]))
|
||||
.allow_methods([Method::GET, Method::POST, Method::DELETE])
|
||||
.allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
|
||||
|
||||
// 照片接口接收 base64 上传,需放宽 body 上限到 16MB;其余接口保持
|
||||
@ -56,6 +57,11 @@ pub fn router(state: AppState) -> Router {
|
||||
let state_routes = Router::new()
|
||||
.route("/api/web/folder/{name}/state", get(get_folder_state).put(save_folder_state));
|
||||
|
||||
// 博客文章路由(正文是文本,默认 2MB body 上限足够)。
|
||||
let post_routes = Router::new()
|
||||
.route("/api/web/posts", get(list_posts).post(create_post))
|
||||
.route("/api/web/posts/{id}", get(get_post).put(update_post).delete(delete_post));
|
||||
|
||||
Router::new()
|
||||
.route("/api/web/stats", get(stats))
|
||||
.route("/api/web/gate", post(gate))
|
||||
@ -64,6 +70,7 @@ pub fn router(state: AppState) -> Router {
|
||||
.merge(photo_routes)
|
||||
.merge(folder_routes)
|
||||
.merge(state_routes)
|
||||
.merge(post_routes)
|
||||
.layer(cors)
|
||||
.with_state(state)
|
||||
}
|
||||
@ -465,3 +472,85 @@ async fn save_folder_state(
|
||||
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
/* ════════════════════════════════════════════════════════════════
|
||||
博客文章接口(按用户隔离,全部 AuthUser 鉴权)
|
||||
════════════════════════════════════════════════════════════════ */
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PostListResponse {
|
||||
items: Vec<Post>,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PostInput {
|
||||
title: String,
|
||||
tag: String,
|
||||
body: String,
|
||||
}
|
||||
|
||||
/// PostError → HTTP 状态码(与 photos 同构)。
|
||||
fn post_status(e: PostError) -> StatusCode {
|
||||
match e {
|
||||
PostError::Full => StatusCode::CONFLICT,
|
||||
PostError::TooLarge => StatusCode::PAYLOAD_TOO_LARGE,
|
||||
PostError::BadInput | PostError::BadUser => StatusCode::BAD_REQUEST,
|
||||
PostError::NotFound => StatusCode::NOT_FOUND,
|
||||
PostError::Io => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_posts(AuthUser(user): AuthUser, State(state): State<AppState>) -> Json<PostListResponse> {
|
||||
Json(PostListResponse {
|
||||
items: state.posts.list(&user),
|
||||
max: MAX_POSTS,
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_post(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<PostInput>,
|
||||
) -> Result<(StatusCode, Json<Post>), StatusCode> {
|
||||
let draft = PostDraft { title: req.title, tag: req.tag, body: req.body };
|
||||
state
|
||||
.posts
|
||||
.create(&user, draft)
|
||||
.map(|p| (StatusCode::CREATED, Json(p)))
|
||||
.map_err(post_status)
|
||||
}
|
||||
|
||||
async fn get_post(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
AxPath(id): AxPath<String>,
|
||||
) -> Result<Json<Post>, StatusCode> {
|
||||
state.posts.read(&user, &id).map(Json).ok_or(StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
async fn update_post(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
AxPath(id): AxPath<String>,
|
||||
Json(req): Json<PostInput>,
|
||||
) -> Result<StatusCode, StatusCode> {
|
||||
let draft = PostDraft { title: req.title, tag: req.tag, body: req.body };
|
||||
state
|
||||
.posts
|
||||
.update(&user, &id, draft)
|
||||
.map(|_| StatusCode::NO_CONTENT)
|
||||
.map_err(post_status)
|
||||
}
|
||||
|
||||
async fn delete_post(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
AxPath(id): AxPath<String>,
|
||||
) -> StatusCode {
|
||||
if state.posts.delete(&user, &id) {
|
||||
StatusCode::NO_CONTENT
|
||||
} else {
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ use crate::auth::{self, DevGate, UserStore, WallUserStore};
|
||||
use crate::folders::{FolderRegistry, FolderStore};
|
||||
use crate::monitor::Stats;
|
||||
use crate::photos::PhotoStore;
|
||||
use crate::posts::PostStore;
|
||||
use crate::weather::WeatherService;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -31,6 +32,8 @@ pub struct AppState {
|
||||
pub folders: FolderRegistry,
|
||||
/// 特殊文件夹图片仓库(按文件夹隔离的磁盘目录)。
|
||||
pub folder_photos: FolderStore,
|
||||
/// 个人博客文章仓库(按用户隔离的磁盘目录)。
|
||||
pub posts: PostStore,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -47,6 +50,7 @@ impl AppState {
|
||||
photos: PhotoStore::from_env(),
|
||||
folders: FolderRegistry::from_env(),
|
||||
folder_photos: FolderStore::from_env(),
|
||||
posts: PostStore::from_env(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user