diff --git a/easytier-web/frontend-lib/src/locales/cn.yaml b/easytier-web/frontend-lib/src/locales/cn.yaml index c713d37b..742afc29 100644 --- a/easytier-web/frontend-lib/src/locales/cn.yaml +++ b/easytier-web/frontend-lib/src/locales/cn.yaml @@ -377,7 +377,6 @@ web: change_password: 修改密码 old_password: 旧密码 new_password: 新密码 - new_password_empty: 新密码不能为空 confirm_password: 确认新密码 language: 语言 theme: 主题 diff --git a/easytier-web/frontend-lib/src/locales/en.yaml b/easytier-web/frontend-lib/src/locales/en.yaml index 4b751ccf..a4449325 100644 --- a/easytier-web/frontend-lib/src/locales/en.yaml +++ b/easytier-web/frontend-lib/src/locales/en.yaml @@ -377,7 +377,6 @@ web: change_password: Change Password old_password: Old Password new_password: New Password - new_password_empty: New password cannot be empty confirm_password: Confirm New Password language: Language theme: Theme diff --git a/easytier-web/frontend/src/modules/api.ts b/easytier-web/frontend/src/modules/api.ts index 848902f5..a0df0eac 100644 --- a/easytier-web/frontend/src/modules/api.ts +++ b/easytier-web/frontend/src/modules/api.ts @@ -164,10 +164,21 @@ export class ApiClient { } public async check_login_status(): Promise { - const response = await this.client.get('/auth/check_login_status'); - return { - loggedIn: true, - mustChangePassword: response.must_change_password, + try { + const response = await this.client.get('/auth/check_login_status'); + return { + loggedIn: true, + mustChangePassword: response.must_change_password, + }; + } catch (error) { + if (error instanceof AxiosError && error.response?.status === 401) { + return { + loggedIn: false, + mustChangePassword: false, + }; + } + + throw error; }; } diff --git a/easytier-web/src/migrator/m20260405_000003_add_must_change_password.rs b/easytier-web/src/migrator/m20260405_000003_add_must_change_password.rs index 96ea6897..2f565f18 100644 --- a/easytier-web/src/migrator/m20260405_000003_add_must_change_password.rs +++ b/easytier-web/src/migrator/m20260405_000003_add_must_change_password.rs @@ -2,10 +2,16 @@ use sea_orm_migration::prelude::*; 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)] enum Users { Table, Username, + Password, MustChangePassword, } @@ -37,7 +43,14 @@ impl MigrationTrait for Migration { Query::update() .table(Users::Table) .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(), ) .await?; @@ -58,3 +71,59 @@ impl MigrationTrait for Migration { 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); + } +} diff --git a/easytier-web/src/restful/auth.rs b/easytier-web/src/restful/auth.rs index 5cb62cf8..b99015d3 100644 --- a/easytier-web/src/restful/auth.rs +++ b/easytier-web/src/restful/auth.rs @@ -60,13 +60,16 @@ mod put { .await { tracing::error!("Failed to change password: {:?}", e); - let status = match e { - ChangePasswordError::EmptyPassword => StatusCode::BAD_REQUEST, - ChangePasswordError::UserNotFound | ChangePasswordError::Db(_) => { - StatusCode::INTERNAL_SERVER_ERROR + let (status, message) = match &e { + ChangePasswordError::EmptyPassword => { + (StatusCode::BAD_REQUEST, "password cannot be empty") } + 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;