diff --git a/doc/api/auth.zh_CN.md b/doc/api/auth.zh_CN.md index fb58d4a..9bbd38f 100644 --- a/doc/api/auth.zh_CN.md +++ b/doc/api/auth.zh_CN.md @@ -11,16 +11,20 @@ * 路径: `/api/auth/user/add`、 `/auth/user/add` * 方法: `GET` 或 `POST` * RESTful: `PUT /api/auth/user` 或 `PUT /auth/user` -* 鉴权: 一般需要(如服务器状态内的`has_root_user`为`false`则无需鉴权) +* 鉴权: 一般需要(如服务器状态内的`has_root_user`为`false`则无需鉴权,如需要仅限管理员) ## 更新用户 * 路径: `/api/auth/user/update`、 `/auth/user/update` * 方法: `GET` 或 `POST` * RESTful: `PATCH /api/auth/user` 或 `PATCH /auth/user` -* 鉴权: 需要 +* 鉴权: 需要(仅管理员) ## 修改用户名字 * 路径: `/api/auth/user/change/name`、 `/auth/user/change/name` * 方法: `GET` 或 `POST` * 鉴权: 需要 +## 修改用户密码 +* 路径: `/api/auth/user/change/password`、 `/auth/user/change/password` +* 方法: `GET` 或 `POST` +* 鉴权: 需要 ## 获取Token * 路径: `/api/auth/token/add`、 `/auth/token/add` * 方法: `GET` 或 `POST` diff --git a/src/db/sqlite/db.rs b/src/db/sqlite/db.rs index 04b724b..66dedcb 100644 --- a/src/db/sqlite/db.rs +++ b/src/db/sqlite/db.rs @@ -434,10 +434,29 @@ impl PixivDownloaderSqlite { )?) } + #[cfg(feature = "server")] fn _update_user_name(ts: &Transaction, id: u64, name: &str) -> Result { Ok(ts.execute("UPDATE users SET name = ? WHERE id = ?;", (name, id))?) } + #[cfg(feature = "server")] + fn _update_user_password( + ts: &Transaction, + id: u64, + password: &[u8], + token_id: u64, + ) -> Result<(), SqliteError> { + ts.execute( + "UPDATE users SET password = ? WHERE id = ?;", + (password, id), + )?; + ts.execute( + "DELETE FROM token WHERE user_id = ? AND id != ?;", + (id, token_id), + )?; + Ok(()) + } + fn _write_version<'a>(&self, ts: &Transaction<'a>) -> Result<(), SqliteError> { let mut stmt = ts.prepare( "INSERT OR REPLACE INTO version (id, v1, v2, v3, v4) VALUES ('main', ?, ?, ?, ?);", @@ -608,4 +627,20 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { } Ok(self.get_user(id).await?.expect("User not found:")) } + + #[cfg(feature = "server")] + async fn update_user_password( + &self, + id: u64, + password: &[u8], + token_id: u64, + ) -> Result { + { + let mut db = self.db.lock().await; + let mut tx = db.transaction()?; + Self::_update_user_password(&mut tx, id, password, token_id)?; + tx.commit()?; + } + Ok(self.get_user(id).await?.expect("User not found:")) + } } diff --git a/src/db/traits.rs b/src/db/traits.rs index 35d20c6..3af5e97 100644 --- a/src/db/traits.rs +++ b/src/db/traits.rs @@ -116,4 +116,17 @@ pub trait PixivDownloaderDb { /// * `id`: The user's ID /// * `name`: The user's name async fn update_user_name(&self, id: u64, name: &str) -> Result; + #[cfg(feature = "server")] + /// Update a user's password + /// * `id`: The user's ID + /// * `password`: The user's hashed password + /// * `token_id`: The token ID + /// # Note + /// All tokens of the user except token_id will be revoked + async fn update_user_password( + &self, + id: u64, + password: &[u8], + token_id: u64, + ) -> Result; } diff --git a/src/server/auth/user.rs b/src/server/auth/user.rs index b0b0741..0c67a83 100644 --- a/src/server/auth/user.rs +++ b/src/server/auth/user.rs @@ -13,6 +13,8 @@ pub enum AuthUserAction { Add, /// Change a user's name. ChangeName, + /// Change a user's password. + ChangePassword, /// Update a existed user. Update, } @@ -53,6 +55,11 @@ impl AuthUserContext { } else { None }; + if let Some(act) = self.action.as_ref() { + if root_user.is_none() && !matches!(act, AuthUserAction::Add) { + return Err((19, gettext("No root user, you need add a user first.")).into()); + } + } match &self.action { Some(act) => match act { AuthUserAction::Add => { @@ -165,6 +172,55 @@ impl AuthUserContext { .try_err3(-1001, gettext("Failed to operate the database:"))?; Ok(user.to_json2()) } + AuthUserAction::ChangePassword => { + let token_id = match req.headers().get("X-TOKEN-ID") { + Some(s) => s.to_str().unwrap().to_owned(), + None => params.get("token_id").unwrap().to_owned(), + } + .parse::() + .unwrap(); + let password = params + .get("password") + .try_err3(3, "No password specified.")?; + let password = base64::decode(password) + .try_err3(4, gettext("Failed to decode password with base64:"))?; + let rsa_key = self.ctx.rsa_key.lock().await; + match *rsa_key { + Some(ref key) => { + if key.is_too_old() { + return Err(( + 6, + gettext("RSA key is too old. A new key should be generated."), + ) + .into()); + } + let password = key + .decrypt(&password) + .try_err3(7, gettext("Failed to decrypt password with RSA:"))?; + let mut hashed_password = [0; 64]; + pbkdf2_hmac( + &password, + &PASSWORD_SALT, + PASSWORD_ITER, + MessageDigest::sha512(), + &mut hashed_password, + ) + .try_err3(11, gettext("Failed to hash password:"))?; + let user = self + .ctx + .db + .update_user_password( + user.expect("User not found:").id, + &hashed_password, + token_id, + ) + .await + .try_err3(8, gettext("Failed to update user in database:"))?; + Ok(user.to_json2()) + } + None => Err((5, gettext("No RSA key found.")).into()), + } + } AuthUserAction::Update => { if root_user.is_some() { if !user.as_ref().expect("User not found:").is_admin { @@ -308,7 +364,8 @@ pub struct AuthUserRoute { impl AuthUserRoute { pub fn new() -> Self { Self { - regex: Regex::new(r"^(/+api)?/+auth/+user(/+(add|update|change/+name))?$").unwrap(), + regex: Regex::new(r"^(/+api)?/+auth/+user(/+(add|update|change/+(name|password)))?$") + .unwrap(), } } } @@ -344,6 +401,7 @@ impl MatchRoute for AuthUserRoute { let m = m.trim_start_matches("/"); match m { "name" => Some(AuthUserAction::ChangeName), + "password" => Some(AuthUserAction::ChangePassword), _ => return None, } } else { diff --git a/src/server/unittest/auth.rs b/src/server/unittest/auth.rs index d532e7b..ac94a6f 100644 --- a/src/server/unittest/auth.rs +++ b/src/server/unittest/auth.rs @@ -9,6 +9,17 @@ use openssl::rsa::{Padding, Rsa}; /// Test authentification methods /// Returns token pub async fn test(ctx: &UnitTestContext) -> Result<[(u64, Vec); 2], PixivDownloaderError> { + let re = ctx + .request_json2( + "/auth/user/change/name", + &json::object! { + "name": "test", + }, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(&re)?.unwrap_err(); + assert_eq!(result.code, 19); let re = Request::builder().uri("/auth").body(Body::empty())?; let res = ctx.request_json(re).await?.unwrap(); assert_eq!(res["has_root_user"].as_bool(), Some(false)); @@ -279,5 +290,78 @@ pub async fn test(ctx: &UnitTestContext) -> Result<[(u64, Vec); 2], PixivDow "is_admin": false, } ); + let re = ctx + .request_json2( + "/auth/token/add", + &json::object! { + "username": "test1", + "password": b64_password2.as_str(), + }, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.expect("Failed to add token:"); + assert_eq!(Some(1), result["user_id"].as_u64()); + let token3 = base64::decode(result["token"].as_str().unwrap()).unwrap(); + assert_eq!(token2.len(), 64); + let token3_id = result["id"].as_u64().unwrap(); + openssl::rand::rand_bytes(&mut password2)?; + key.public_encrypt(&password2, &mut encypted2, Padding::PKCS1)?; + let b64_password2 = base64::encode(&encypted2); + let re = ctx + .request_json2_sign( + "/auth/user/change/password", + &json::object! { + "password": b64_password2.as_str(), + }, + &token2, + token2_id, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.unwrap(); + assert_eq!( + result, + json::object! { + "id": 1, + "name": "sdlkasdjklasjd", + "username": "test1", + "is_admin": false, + } + ); + let re = ctx + .request_json2_sign( + "/auth/user/change/name", + &json::object! { + "name": "sadiuqwed", + }, + &token3, + token3_id, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.unwrap_err(); + assert_eq!(result.code, -403); + let re = ctx + .request_json2_sign( + "/auth/user/change/name", + &json::object! { + "name": "sadiuqwed", + }, + &token2, + token2_id, + ) + .await? + .unwrap(); + let result = JSONResult::from_json(re)?.unwrap(); + assert_eq!( + result, + json::object! { + "id": 1, + "name": "sadiuqwed", + "username": "test1", + "is_admin": false, + } + ); Ok([(token_id, token), (token2_id, token2)]) }