diff --git a/doc/api/auth.zh_CN.md b/doc/api/auth.zh_CN.md index 3cb733c..db02ab3 100644 --- a/doc/api/auth.zh_CN.md +++ b/doc/api/auth.zh_CN.md @@ -12,6 +12,11 @@ * 方法: `GET` 或 `POST` * RESTful: `PUT /api/auth/user` 或 `PUT /auth/user` * 鉴权: 一般需要(如服务器状态内的`has_root_user`为`false`则无需鉴权) +## 更新用户 +* 路径: `/api/auth/user/update`、 `/auth/user/update` +* 方法: `GET` 或 `POST` +* RESTful: `PATCH /api/auth/user` 或 `PATCH /auth/user` +* 鉴权: 需要 ## 获取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 e9e9aa1..ad26bd7 100644 --- a/src/db/sqlite/db.rs +++ b/src/db/sqlite/db.rs @@ -404,6 +404,36 @@ impl PixivDownloaderSqlite { } } + #[cfg(feature = "server")] + async fn _update_user( + &self, + id: u64, + name: &str, + username: &str, + password: &[u8], + is_admin: bool, + ) -> Result { + let con = self.db.lock().await; + let has_username = con + .query_row( + "SELECT * FROM users WHERE username = ?;", + [username], + |row| { + let uid: u64 = row.get(0)?; + Ok(uid != id) + }, + ) + .optional()? + .unwrap_or(false); + if has_username { + return Err(SqliteError::UserNameAlreadyExists); + } + Ok(con.execute( + "UPDATE users SET name = ?, username = ?, password = ?, is_admin = ? WHERE id = ?;", + (name, username, password, is_admin, id), + )?) + } + 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', ?, ?, ?, ?);", @@ -549,4 +579,18 @@ impl PixivDownloaderDb for PixivDownloaderSqlite { ._set_user(id, name, username, password, is_admin) .await?) } + + #[cfg(feature = "server")] + async fn update_user( + &self, + id: u64, + name: &str, + username: &str, + password: &[u8], + is_admin: bool, + ) -> Result { + self._update_user(id, name, username, password, is_admin) + .await?; + Ok(self.get_user(id).await?.expect("User not found:")) + } } diff --git a/src/db/traits.rs b/src/db/traits.rs index 68577e7..2e22e33 100644 --- a/src/db/traits.rs +++ b/src/db/traits.rs @@ -93,4 +93,22 @@ pub trait PixivDownloaderDb { password: &[u8], is_admin: bool, ) -> Result; + #[cfg(feature = "server")] + /// Update a user's information + /// * `id`: The user's ID + /// * `name`: The user's name + /// * `username`: The user's username + /// * `password`: The user's hashed password + /// * `is_admin`: Whether the user is an admin + /// # Note + /// If the user does not exist, the operation must failed. + /// If the user's `username` is taked by another user, the operation must failed. + async fn update_user( + &self, + id: u64, + name: &str, + username: &str, + password: &[u8], + is_admin: bool, + ) -> Result; } diff --git a/src/server/auth/user.rs b/src/server/auth/user.rs index 3d9e243..92b2c0b 100644 --- a/src/server/auth/user.rs +++ b/src/server/auth/user.rs @@ -3,6 +3,7 @@ use super::{PASSWORD_ITER, PASSWORD_SALT}; use crate::ext::json::ToJson2; use crate::ext::try_err::{TryErr, TryErr3}; use crate::gettext; +use bytes::BytesMut; use openssl::{hash::MessageDigest, pkcs5::pbkdf2_hmac}; #[derive(Clone, Debug)] @@ -10,6 +11,8 @@ use openssl::{hash::MessageDigest, pkcs5::pbkdf2_hmac}; pub enum AuthUserAction { /// Add a new user. Add, + /// Update a existed user. + Update, } pub struct AuthUserContext { @@ -150,6 +153,99 @@ impl AuthUserContext { None => return 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 { + return Err((9, gettext("Admin privileges required.")).into()); + } + } + let id = params.get_u64("id").try_err3( + 13, + &gettext("Failed to parse :").replace("", "user id"), + )?; + let username = params.get("username"); + let mut user = if let Some(id) = id { + self.ctx.db.get_user(id).await + } else if let Some(username) = username { + self.ctx.db.get_user_by_username(username).await + } else { + return Err((14, gettext("No user id or username specified.")).into()); + } + .try_err3(-1001, gettext("Failed to operate the database:"))? + .ok_or((15, gettext("User not found.")))?; + if let Some(username) = username { + if username != user.username { + user.username = username.to_owned(); + } + } + if let Some(name) = params.get("name") { + if name != user.name { + user.name = name.to_owned(); + } + } + if let Some(password) = params.get("password") { + 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 pw: &[u8] = &user.password; + if &hashed_password != pw { + let pw: &[u8] = &hashed_password; + user.password = BytesMut::from(pw); + } + } + None => return Err((5, gettext("No RSA key found.")).into()), + } + } + if let Some(is_admin) = params.get_bool("is_admin").try_err3( + 16, + &gettext("Failed to parse :").replace("", "is_admin"), + )? { + if user.id == 0 && !is_admin { + return Err(( + 17, + gettext("Cannot change admin privileges of root user."), + ) + .into()); + } + user.is_admin = is_admin; + } + let user = self + .ctx + .db + .update_user( + user.id, + &user.name, + &user.username, + &user.password, + user.is_admin, + ) + .await + .try_err3(-1001, gettext("Failed to operate the database:"))?; + Ok(user.to_json2()) + } }, None => { panic!("No action specified for AuthUserContext."); @@ -200,7 +296,7 @@ pub struct AuthUserRoute { impl AuthUserRoute { pub fn new() -> Self { Self { - regex: Regex::new(r"^(/+api)?/+auth/+user(/+add)?$").unwrap(), + regex: Regex::new(r"^(/+api)?/+auth/+user(/+(add|update))?$").unwrap(), } } } @@ -229,12 +325,16 @@ impl MatchRoute for AuthUserRoute { let m = m.as_str().trim_start_matches("/"); match m { "add" => Some(AuthUserAction::Add), + "update" => Some(AuthUserAction::Update), _ => return None, } } None => { - if req.method() == Method::PUT { + let m = req.method(); + if m == Method::PUT { Some(AuthUserAction::Add) + } else if m == Method::PATCH { + Some(AuthUserAction::Update) } else { None }