feat(web): per-user photo endpoints (list/upload/get/delete) behind JWT
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
29cbf524e9
commit
e8f3f1674d
@ -8,10 +8,15 @@ use axum::{
|
||||
http::{HeaderMap, Method, StatusCode, header},
|
||||
routing::{get, post},
|
||||
};
|
||||
use axum::extract::{DefaultBodyLimit, Path as AxPath};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||
|
||||
use crate::photos::{SaveError, MAX_PHOTOS};
|
||||
use crate::routes::extract::AuthUser;
|
||||
|
||||
use crate::{
|
||||
monitor::Stats,
|
||||
state::AppState,
|
||||
@ -29,14 +34,17 @@ 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])
|
||||
.allow_headers([header::CONTENT_TYPE]);
|
||||
.allow_methods([Method::GET, Method::POST, Method::DELETE])
|
||||
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
|
||||
|
||||
Router::new()
|
||||
.route("/api/web/stats", get(stats))
|
||||
.route("/api/web/gate", post(gate))
|
||||
.route("/api/web/login", post(login))
|
||||
.route("/api/web/weather", get(weather))
|
||||
.route("/api/web/photos", get(list_photos).post(upload_photo))
|
||||
.route("/api/web/photos/{id}", get(get_photo).delete(delete_photo))
|
||||
.layer(DefaultBodyLimit::max(16 * 1024 * 1024))
|
||||
.layer(cors)
|
||||
.with_state(state)
|
||||
}
|
||||
@ -178,3 +186,76 @@ fn client_ip(headers: &HeaderMap, addr: SocketAddr) -> String {
|
||||
}
|
||||
addr.ip().to_string()
|
||||
}
|
||||
|
||||
/// 当前用户的图片 id 列表 + 数量上限。
|
||||
#[derive(Serialize)]
|
||||
struct PhotoList {
|
||||
items: Vec<PhotoItem>,
|
||||
max: usize,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct PhotoItem {
|
||||
id: String,
|
||||
}
|
||||
|
||||
async fn list_photos(AuthUser(user): AuthUser, State(state): State<AppState>) -> Json<PhotoList> {
|
||||
let items = state
|
||||
.photos
|
||||
.list(&user)
|
||||
.into_iter()
|
||||
.map(|id| PhotoItem { id })
|
||||
.collect();
|
||||
Json(PhotoList {
|
||||
items,
|
||||
max: MAX_PHOTOS,
|
||||
})
|
||||
}
|
||||
|
||||
/// 上传请求体:base64 data URL(前端 FileReader 读出的)。
|
||||
#[derive(Deserialize)]
|
||||
struct UploadRequest {
|
||||
#[serde(rename = "dataUrl")]
|
||||
data_url: String,
|
||||
}
|
||||
#[derive(Serialize)]
|
||||
struct UploadResponse {
|
||||
id: String,
|
||||
}
|
||||
|
||||
async fn upload_photo(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<UploadRequest>,
|
||||
) -> Result<(StatusCode, Json<UploadResponse>), StatusCode> {
|
||||
match state.photos.save(&user, &req.data_url) {
|
||||
Ok(id) => Ok((StatusCode::CREATED, Json(UploadResponse { id }))),
|
||||
Err(SaveError::Full) => Err(StatusCode::CONFLICT),
|
||||
Err(SaveError::TooLarge) => Err(StatusCode::PAYLOAD_TOO_LARGE),
|
||||
Err(SaveError::BadType) => Err(StatusCode::UNSUPPORTED_MEDIA_TYPE),
|
||||
Err(SaveError::BadUser) => Err(StatusCode::BAD_REQUEST),
|
||||
Err(SaveError::Io) => Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_photo(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
AxPath(id): AxPath<String>,
|
||||
) -> Response {
|
||||
match state.photos.read(&user, &id) {
|
||||
Some((mime, bytes)) => ([(header::CONTENT_TYPE, mime)], bytes).into_response(),
|
||||
None => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_photo(
|
||||
AuthUser(user): AuthUser,
|
||||
State(state): State<AppState>,
|
||||
AxPath(id): AxPath<String>,
|
||||
) -> StatusCode {
|
||||
if state.photos.delete(&user, &id) {
|
||||
StatusCode::NO_CONTENT
|
||||
} else {
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ use tokio::sync::RwLock;
|
||||
|
||||
use crate::auth::{self, DevGate, UserStore};
|
||||
use crate::monitor::Stats;
|
||||
use crate::photos::PhotoStore;
|
||||
use crate::weather::WeatherService;
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -21,6 +22,8 @@ pub struct AppState {
|
||||
pub gate: Arc<DevGate>,
|
||||
/// 天气/定位服务(自带每日限流 + 缓存)。
|
||||
pub weather: Arc<WeatherService>,
|
||||
/// 照片墙图片仓库(按用户隔离的磁盘目录)。
|
||||
pub photos: PhotoStore,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
@ -32,6 +35,7 @@ impl AppState {
|
||||
users: Arc::new(UserStore::new(pool.clone())),
|
||||
gate: Arc::new(DevGate::new(pool)),
|
||||
weather: Arc::new(WeatherService::from_env()),
|
||||
photos: PhotoStore::from_env(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user