feat(admin): 登录改页内弹窗 + P1 站点设置(页头/侧栏)
- 登录:LoginDialog(Modal)替代 /login 页面;/login 重定向回首页 - 设置接口:GET /api/settings 公开读、PATCH 需登录 + version 乐观锁(409/400) - setting 表存 site JSON:页头(启用/速度/条目+标签+色调)、侧栏(hidden/order) - /admin:Tabs 页头/侧栏,条目增删与上下移,保存后首页生效 - 首页应用设置:ticker 文本占位符与速度、侧栏顺序与显隐 - 测试 42 passed(settings 5 例)
This commit is contained in:
@@ -534,6 +534,53 @@ async fn auth_password(
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 站点设置 ----------------
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SettingsPatch {
|
||||
version: i64,
|
||||
settings: crate::db::Settings,
|
||||
}
|
||||
|
||||
/// GET /api/settings —— 读接口公开,前端首屏直接取
|
||||
async fn get_settings(State(state): State<AppState>) -> Response {
|
||||
let db = Arc::clone(&state.db);
|
||||
match tokio::task::spawn_blocking(move || db.load_settings()).await {
|
||||
Ok(Ok(document)) => Json(document).into_response(),
|
||||
Ok(Err(error)) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
|
||||
Err(error) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /api/settings —— 需要登录;`version` 做乐观锁
|
||||
async fn patch_settings(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Json(body): Json<SettingsPatch>,
|
||||
) -> Response {
|
||||
if !same_origin(&headers) {
|
||||
return api_error(StatusCode::FORBIDDEN, "跨站请求被拒绝");
|
||||
}
|
||||
|
||||
match read_session(&state, &headers).await {
|
||||
Ok(session) if session.authenticated => {}
|
||||
Ok(_) => return api_error(StatusCode::UNAUTHORIZED, "请先登录"),
|
||||
Err(response) => return response,
|
||||
}
|
||||
|
||||
let db = Arc::clone(&state.db);
|
||||
let (settings, version) = (body.settings, body.version);
|
||||
match tokio::task::spawn_blocking(move || db.save_settings(settings, version)).await {
|
||||
Ok(Ok(document)) => Json(document).into_response(),
|
||||
Ok(Err(StashError::Conflict(message))) => api_error(StatusCode::CONFLICT, &message),
|
||||
Ok(Err(StashError::InvalidDirectory(message))) => {
|
||||
api_error(StatusCode::BAD_REQUEST, &message)
|
||||
}
|
||||
Ok(Err(error)) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
|
||||
Err(error) => api_error(StatusCode::INTERNAL_SERVER_ERROR, &error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 组装路由:API + 内嵌前端 + 托管目录。
|
||||
/// 单独抽出来是为了测试能直接对 `Router` 发请求:`start` 要绑定端口、
|
||||
/// 起监听任务,没法在单元测试里用。
|
||||
@@ -559,6 +606,7 @@ fn build_router(state: AppState) -> Router {
|
||||
.route("/api/auth/login", post(auth_login))
|
||||
.route("/api/auth/logout", post(auth_logout))
|
||||
.route("/api/auth/password", patch(auth_password))
|
||||
.route("/api/settings", get(get_settings).patch(patch_settings))
|
||||
.route("/", get(web_index))
|
||||
.route("/index.html", get(web_index))
|
||||
.route("/favicon.svg", get(web_root_file))
|
||||
|
||||
Reference in New Issue
Block a user