fix(web): avoid false default-password reminders

Only flag seeded accounts that still use the shipped password hash,
and keep auth status and password change responses stable during
review follow-up.
This commit is contained in:
fanyang
2026-04-05 17:52:11 +08:00
parent 7707b1cf5e
commit 349dbf7d8d
5 changed files with 93 additions and 12 deletions
@@ -377,7 +377,6 @@ web:
change_password: 修改密码 change_password: 修改密码
old_password: 旧密码 old_password: 旧密码
new_password: 新密码 new_password: 新密码
new_password_empty: 新密码不能为空
confirm_password: 确认新密码 confirm_password: 确认新密码
language: 语言 language: 语言
theme: 主题 theme: 主题
@@ -377,7 +377,6 @@ web:
change_password: Change Password change_password: Change Password
old_password: Old Password old_password: Old Password
new_password: New Password new_password: New Password
new_password_empty: New password cannot be empty
confirm_password: Confirm New Password confirm_password: Confirm New Password
language: Language language: Language
theme: Theme theme: Theme
+11
View File
@@ -164,11 +164,22 @@ export class ApiClient {
} }
public async check_login_status(): Promise<CheckLoginStatusResponse> { public async check_login_status(): Promise<CheckLoginStatusResponse> {
try {
const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status'); const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status');
return { return {
loggedIn: true, loggedIn: true,
mustChangePassword: response.must_change_password, mustChangePassword: response.must_change_password,
}; };
} catch (error) {
if (error instanceof AxiosError && error.response?.status === 401) {
return {
loggedIn: false,
mustChangePassword: false,
};
}
throw error;
};
} }
public async list_session() { public async list_session() {
@@ -2,10 +2,16 @@ use sea_orm_migration::prelude::*;
pub struct Migration; pub struct Migration;
const DEFAULT_USER_PASSWORD_HASH: &str =
"$argon2i$v=19$m=16,t=2,p=1$aGVyRDBrcnRycnlaMDhkbw$449SEcv/qXf+0fnI9+fYVQ";
const DEFAULT_ADMIN_PASSWORD_HASH: &str =
"$argon2i$v=19$m=16,t=2,p=1$bW5idXl0cmY$61n+JxL4r3dwLPAEDlDdtg";
#[derive(DeriveIden)] #[derive(DeriveIden)]
enum Users { enum Users {
Table, Table,
Username, Username,
Password,
MustChangePassword, MustChangePassword,
} }
@@ -37,7 +43,14 @@ impl MigrationTrait for Migration {
Query::update() Query::update()
.table(Users::Table) .table(Users::Table)
.value(Users::MustChangePassword, true) .value(Users::MustChangePassword, true)
.and_where(Expr::col(Users::Username).is_in(["admin", "user"])) .cond_where(any![
Expr::col(Users::Username)
.eq("admin")
.and(Expr::col(Users::Password).eq(DEFAULT_ADMIN_PASSWORD_HASH)),
Expr::col(Users::Username)
.eq("user")
.and(Expr::col(Users::Password).eq(DEFAULT_USER_PASSWORD_HASH)),
])
.to_owned(), .to_owned(),
) )
.await?; .await?;
@@ -58,3 +71,59 @@ impl MigrationTrait for Migration {
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter as _, SqlxSqliteConnector};
use sea_orm_migration::prelude::SchemaManager;
use sqlx::sqlite::SqlitePoolOptions;
use super::{Migration, MigrationTrait, DEFAULT_USER_PASSWORD_HASH};
use crate::db::entity::users;
async fn find_user(db: &sea_orm::DatabaseConnection, username: &str) -> users::Model {
users::Entity::find()
.filter(users::Column::Username.eq(username))
.one(db)
.await
.unwrap()
.unwrap()
}
#[tokio::test]
async fn migration_only_marks_seeded_accounts_still_using_default_passwords() {
let pool = SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query(
"CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)",
)
.execute(&pool)
.await
.unwrap();
let changed_admin_password = password_auth::generate_hash("already-changed");
sqlx::query("INSERT INTO users (username, password) VALUES (?, ?), (?, ?)")
.bind("admin")
.bind(changed_admin_password)
.bind("user")
.bind(DEFAULT_USER_PASSWORD_HASH)
.execute(&pool)
.await
.unwrap();
let db = SqlxSqliteConnector::from_sqlx_sqlite_pool(pool);
Migration.up(&SchemaManager::new(&db)).await.unwrap();
assert!(!find_user(&db, "admin").await.must_change_password);
assert!(find_user(&db, "user").await.must_change_password);
}
}
+8 -5
View File
@@ -60,13 +60,16 @@ mod put {
.await .await
{ {
tracing::error!("Failed to change password: {:?}", e); tracing::error!("Failed to change password: {:?}", e);
let status = match e { let (status, message) = match &e {
ChangePasswordError::EmptyPassword => StatusCode::BAD_REQUEST, ChangePasswordError::EmptyPassword => {
ChangePasswordError::UserNotFound | ChangePasswordError::Db(_) => { (StatusCode::BAD_REQUEST, "password cannot be empty")
StatusCode::INTERNAL_SERVER_ERROR
} }
ChangePasswordError::UserNotFound | ChangePasswordError::Db(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"failed to change password",
),
}; };
return Err((status, Json::from(other_error(format!("{:?}", e))))); return Err((status, Json::from(other_error(message.to_string()))));
} }
let _ = auth_session.logout().await; let _ = auth_session.logout().await;