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},
|
http::{HeaderMap, Method, StatusCode, header},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
use axum::extract::{DefaultBodyLimit, Path as AxPath};
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use tower_http::cors::{AllowOrigin, CorsLayer};
|
use tower_http::cors::{AllowOrigin, CorsLayer};
|
||||||
|
|
||||||
|
use crate::photos::{SaveError, MAX_PHOTOS};
|
||||||
|
use crate::routes::extract::AuthUser;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
monitor::Stats,
|
monitor::Stats,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@ -29,14 +34,17 @@ pub fn router(state: AppState) -> Router {
|
|||||||
"http://localhost:5174".parse().unwrap(),
|
"http://localhost:5174".parse().unwrap(),
|
||||||
"http://127.0.0.1:5174".parse().unwrap(),
|
"http://127.0.0.1:5174".parse().unwrap(),
|
||||||
]))
|
]))
|
||||||
.allow_methods([Method::GET, Method::POST])
|
.allow_methods([Method::GET, Method::POST, Method::DELETE])
|
||||||
.allow_headers([header::CONTENT_TYPE]);
|
.allow_headers([header::CONTENT_TYPE, header::AUTHORIZATION]);
|
||||||
|
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/web/stats", get(stats))
|
.route("/api/web/stats", get(stats))
|
||||||
.route("/api/web/gate", post(gate))
|
.route("/api/web/gate", post(gate))
|
||||||
.route("/api/web/login", post(login))
|
.route("/api/web/login", post(login))
|
||||||
.route("/api/web/weather", get(weather))
|
.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)
|
.layer(cors)
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
}
|
}
|
||||||
@ -178,3 +186,76 @@ fn client_ip(headers: &HeaderMap, addr: SocketAddr) -> String {
|
|||||||
}
|
}
|
||||||
addr.ip().to_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::auth::{self, DevGate, UserStore};
|
||||||
use crate::monitor::Stats;
|
use crate::monitor::Stats;
|
||||||
|
use crate::photos::PhotoStore;
|
||||||
use crate::weather::WeatherService;
|
use crate::weather::WeatherService;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@ -21,6 +22,8 @@ pub struct AppState {
|
|||||||
pub gate: Arc<DevGate>,
|
pub gate: Arc<DevGate>,
|
||||||
/// 天气/定位服务(自带每日限流 + 缓存)。
|
/// 天气/定位服务(自带每日限流 + 缓存)。
|
||||||
pub weather: Arc<WeatherService>,
|
pub weather: Arc<WeatherService>,
|
||||||
|
/// 照片墙图片仓库(按用户隔离的磁盘目录)。
|
||||||
|
pub photos: PhotoStore,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
@ -32,6 +35,7 @@ impl AppState {
|
|||||||
users: Arc::new(UserStore::new(pool.clone())),
|
users: Arc::new(UserStore::new(pool.clone())),
|
||||||
gate: Arc::new(DevGate::new(pool)),
|
gate: Arc::new(DevGate::new(pool)),
|
||||||
weather: Arc::new(WeatherService::from_env()),
|
weather: Arc::new(WeatherService::from_env()),
|
||||||
|
photos: PhotoStore::from_env(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user