feat(auth): 新增 wall_users 表,照片墙与开发者登录分离

登录先查 ccuser(开发者),再查 wall_users(照片墙用户)。
wall_users 初始用户:cc / UserTest。

Co-Authored-By: Claude <noreply@anthropic.com>
@
This commit is contained in:
cc 2026-06-24 22:42:06 +08:00
parent 8d89cee451
commit 0e6c774ed6
3 changed files with 72 additions and 3 deletions

View File

@ -188,6 +188,69 @@ impl UserStore {
}
}
/// 照片墙用户校验后端:查 `wall_users` 表(与 `ccuser` 物理隔离)。
/// 未设 DATABASE_URL 时退回内置兜底。
pub enum WallUserStore {
Db(MySqlPool),
Memory(HashMap<String, String>),
}
impl WallUserStore {
pub fn new(pool: Option<MySqlPool>) -> Self {
match pool {
Some(p) => {
eprintln!("[auth] 照片墙用户校验走 MySQL (wall_users)");
WallUserStore::Db(p)
}
None => {
eprintln!("[auth] 未设置 DATABASE_URL照片墙用户回退到内置表");
Self::seed()
}
}
}
/// 内置兜底cc + UserTest
pub fn seed() -> Self {
let mut users = HashMap::new();
// cc / 192118Lht
users.insert(
"cc".to_string(),
"0086e83c9b108b227eed55425e9641286f42bd0c31a1b95afbb9edd6d3aa6234".to_string(),
);
// UserTest / 2015.08.607
users.insert(
"UserTest".to_string(),
"76f03f2eaa05e66c82d25e168fec27e7d0d202f15da73aa5852562f863633b8e".to_string(),
);
WallUserStore::Memory(users)
}
pub async fn verify(&self, username: &str, password: &str) -> bool {
let actual_hex = sha256_hex(password);
let expected_hex = match self {
WallUserStore::Memory(users) => users.get(username).cloned(),
WallUserStore::Db(pool) => {
let row: Result<Option<(String,)>, sqlx::Error> =
sqlx::query_as("SELECT password_sha256 FROM wall_users WHERE username = ?")
.bind(username)
.fetch_optional(pool)
.await;
match row {
Ok(found) => found.map(|(hex,)| hex),
Err(e) => {
eprintln!("[auth] 查询 wall_users 失败: {e}");
None
}
}
}
};
match expected_hex {
Some(expected) => constant_time_eq(actual_hex.as_bytes(), expected.as_bytes()),
None => false,
}
}
}
/// JWT 载荷:`sub` 为用户名,`exp` 为到期 Unix 秒。
#[derive(Serialize, Deserialize)]
struct Claims {

View File

@ -117,7 +117,10 @@ async fn login(
State(state): State<AppState>,
Json(req): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, StatusCode> {
if state.users.verify(&req.username, &req.password).await {
// 先查开发者表 ccuser再查照片墙用户表 wall_users
if state.users.verify(&req.username, &req.password).await
|| state.wall_users.verify(&req.username, &req.password).await
{
Ok(Json(LoginResponse {
ok: true,
username: req.username.clone(),

View File

@ -7,7 +7,7 @@ use std::sync::Arc;
use tokio::sync::RwLock;
use crate::auth::{self, DevGate, UserStore};
use crate::auth::{self, DevGate, UserStore, WallUserStore};
use crate::folders::{FolderRegistry, FolderStore};
use crate::monitor::Stats;
use crate::photos::PhotoStore;
@ -23,6 +23,8 @@ pub struct AppState {
pub gate: Arc<DevGate>,
/// 天气/定位服务(自带每日限流 + 缓存)。
pub weather: Arc<WeatherService>,
/// 照片墙用户校验后端wall_users 表)。
pub wall_users: Arc<WallUserStore>,
/// 照片墙图片仓库(按用户隔离的磁盘目录)。
pub photos: PhotoStore,
/// 特殊文件夹注册表(白名单)。
@ -38,8 +40,9 @@ impl AppState {
Self {
snapshot: Arc::new(RwLock::new(Stats::default())),
users: Arc::new(UserStore::new(pool.clone())),
gate: Arc::new(DevGate::new(pool)),
gate: Arc::new(DevGate::new(pool.clone())),
weather: Arc::new(WeatherService::from_env()),
wall_users: Arc::new(WallUserStore::new(pool.clone())),
photos: PhotoStore::from_env(),
folders: FolderRegistry::from_env(),
folder_photos: FolderStore::from_env(),