feat(auth): P0 登录与会话(迁移 v16、argon2id、HttpOnly Cookie)
- 迁移机制:schema::migrate 支持 15→16,新增 admin/session/setting 表 - 接口:session/setup/login/logout/password;SameSite=Strict + Origin 校验 + 失败限流 - 会话只存 token 的 sha256;改密清空全部会话 - 前端:/login 独立壳(首次=设置密码),页头登录/退出切换 - 测试:37 passed(迁移 + 登录/会话 4 例) - 文档:README 登录说明、docs/ADMIN-PLAN.md 方案与 P0 记录
This commit is contained in:
+79
-22
@@ -8,6 +8,7 @@
|
||||
//! 3. **开发期不做迁移**:表结构还在变,改结构就删掉 `data/stash.db` 重启,
|
||||
//! 启动时会重新建表并灌入内嵌种子。
|
||||
|
||||
pub mod auth;
|
||||
mod catalog;
|
||||
mod schema;
|
||||
|
||||
@@ -21,6 +22,9 @@ use crate::{config::Config, error::StashError};
|
||||
|
||||
pub use schema::SeedReport;
|
||||
|
||||
/// 登录/会话的对外状态(server 层直接用)。
|
||||
pub use auth::SessionState;
|
||||
|
||||
/// 一次查询得到的完整内容目录。
|
||||
///
|
||||
/// 侧栏已改为由这份分类树派生,不再单独下发导航数据。
|
||||
@@ -57,14 +61,14 @@ impl Db {
|
||||
)
|
||||
.map_err(map_error)?;
|
||||
|
||||
// 先校验结构版本,再动任何东西:旧库直接报错,避免用旧表跑新查询
|
||||
let version = schema::check_version(&conn)?;
|
||||
|
||||
schema::apply(&conn)?;
|
||||
|
||||
// 写入当前结构版本,供下次启动比对
|
||||
conn.pragma_update(None, "user_version", schema::SCHEMA_VERSION)
|
||||
.map_err(map_error)?;
|
||||
// 先迁移结构,再动数据:迁移失败会直接报错,不会用半套表跑查询
|
||||
let version = schema::migrate(&conn)?;
|
||||
if version != schema::SCHEMA_VERSION {
|
||||
return Err(StashError::Database(format!(
|
||||
"内容库迁移后版本为 {version},期望 {}",
|
||||
schema::SCHEMA_VERSION
|
||||
)));
|
||||
}
|
||||
|
||||
// 空库才灌种子:用户删掉内置条目后重启不会被重新塞回来
|
||||
let existing: i64 = conn
|
||||
@@ -115,6 +119,39 @@ impl Db {
|
||||
.lock()
|
||||
.map_err(|_| StashError::Database("数据库连接锁已损坏,请重启进程".into()))
|
||||
}
|
||||
|
||||
// ---------- 登录 / 会话 ----------
|
||||
|
||||
pub fn auth_state(&self, token: Option<&str>) -> Result<SessionState, StashError> {
|
||||
let conn = self.lock()?;
|
||||
auth::state(&conn, token)
|
||||
}
|
||||
|
||||
pub fn setup_admin(&self, username: &str, password: &str) -> Result<(), StashError> {
|
||||
let conn = self.lock()?;
|
||||
auth::create_admin(&conn, username, password)
|
||||
}
|
||||
|
||||
pub fn login(&self, password: &str) -> Result<Option<String>, StashError> {
|
||||
let conn = self.lock()?;
|
||||
auth::purge_expired(&conn)?;
|
||||
auth::verify_login(&conn, password)
|
||||
}
|
||||
|
||||
pub fn issue_session(&self) -> Result<String, StashError> {
|
||||
let conn = self.lock()?;
|
||||
auth::issue_session(&conn)
|
||||
}
|
||||
|
||||
pub fn logout(&self, token: &str) -> Result<(), StashError> {
|
||||
let conn = self.lock()?;
|
||||
auth::revoke(&conn, token)
|
||||
}
|
||||
|
||||
pub fn change_password(&self, current: &str, next: &str) -> Result<bool, StashError> {
|
||||
let mut conn = self.lock()?;
|
||||
auth::change_password(&mut conn, current, next)
|
||||
}
|
||||
}
|
||||
|
||||
/// 把 rusqlite 的错误统一收敛成 `StashError::Database`。
|
||||
@@ -446,37 +483,57 @@ mod tests {
|
||||
assert!(db.data_version().expect("应能读取 data_version") >= 0);
|
||||
}
|
||||
|
||||
/// 全新库(user_version = 0)允许直接建表;当前版本也允许。
|
||||
/// 全新库(user_version = 0)建全量结构;当前版本直接返回;
|
||||
/// 15 会迁移到 16;比程序新的版本拒绝。
|
||||
#[test]
|
||||
fn accepts_fresh_or_current_schema_version() {
|
||||
fn migrates_fresh_previous_and_current_schema() {
|
||||
let conn = Connection::open_in_memory().expect("内存库应能打开");
|
||||
|
||||
assert_eq!(
|
||||
schema::check_version(&conn).expect("全新库应放行"),
|
||||
0,
|
||||
"新库的 user_version 应为 0"
|
||||
schema::migrate(&conn).expect("全新库应能建表"),
|
||||
schema::SCHEMA_VERSION,
|
||||
"新库应直接建到当前版本"
|
||||
);
|
||||
|
||||
conn.pragma_update(None, "user_version", schema::SCHEMA_VERSION)
|
||||
.expect("应能写入版本");
|
||||
let fresh = Connection::open_in_memory().expect("内存库应能打开");
|
||||
fresh
|
||||
.execute_batch(
|
||||
"CREATE TABLE node (key TEXT PRIMARY KEY);
|
||||
PRAGMA user_version = 15;",
|
||||
)
|
||||
.expect("应能造出 v15 老库");
|
||||
assert_eq!(
|
||||
schema::check_version(&conn).expect("当前版本应放行"),
|
||||
schema::migrate(&fresh).expect("v15 应能迁移"),
|
||||
schema::SCHEMA_VERSION
|
||||
);
|
||||
|
||||
let migrated: i64 = fresh
|
||||
.pragma_query_value(None, "user_version", |row| row.get(0))
|
||||
.expect("应能读出版本");
|
||||
assert_eq!(migrated, schema::SCHEMA_VERSION);
|
||||
|
||||
let table: i64 = fresh
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('admin','session','setting')",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.expect("应能查表");
|
||||
assert_eq!(table, 3, "迁移后必须有登录/设置三张表");
|
||||
}
|
||||
|
||||
/// 结构变过之后,旧库必须被拒绝并提示删库,而不是用旧表跑新查询。
|
||||
/// 比程序更新的库必须被拒绝,不能按旧结构乱读。
|
||||
#[test]
|
||||
fn rejects_database_from_older_schema() {
|
||||
fn rejects_database_from_newer_schema() {
|
||||
let conn = Connection::open_in_memory().expect("内存库应能打开");
|
||||
conn.pragma_update(None, "user_version", schema::SCHEMA_VERSION - 1)
|
||||
.expect("应能写入旧版本号");
|
||||
conn.pragma_update(None, "user_version", schema::SCHEMA_VERSION + 1)
|
||||
.expect("应能写入新版本号");
|
||||
|
||||
let error = schema::check_version(&conn).expect_err("旧库必须被拒绝");
|
||||
let error = schema::migrate(&conn).expect_err("更新的库必须被拒绝");
|
||||
let message = error.to_string();
|
||||
|
||||
assert!(message.contains("结构版本"), "报错要说明原因: {message}");
|
||||
assert!(message.contains("删除数据库"), "报错要给出处理方式: {message}");
|
||||
assert!(message.contains("升级程序"), "报错要给出处理方式: {message}");
|
||||
}
|
||||
|
||||
/// 回归测试:外部连接提交后,服务端连接读到的 data_version 必须变化。
|
||||
|
||||
Reference in New Issue
Block a user