本章我们将实现管理员服务。

首先创建项目并加入到 workspace 中:

cargo new admin-srv

密码处理

为了保证存储在数据库中的密码安全,我们创建一个 blog-utils crate:

cargo new --lib blog-utils

加上依赖:

pub fn hash(pwd: &str) -> Result<String, String> {
    bcrypt::hash(pwd, bcrypt::DEFAULT_COST).map_err(|err| err.to_string())
}
pub fn verify(pwd: &str, hashed_pwd: &str) -> Result<bool, String> {
    bcrypt::verify(pwd, hashed_pwd).map_err(|err| err.to_string())
}
  • hash:将明文密码进行hash
  • verify:对明文密码和给定的hash过的密码进行验证

管理员是否存在 admin_exists 的实现

根据传入的条件,通过email或id判断管理员是否存在。

获取管理员 get_admin的实现

获取管理员的几种场景:

  • 通过email和密码进行获取,如果email和密码正确,则返回结果。通常用于管理员登录
  • 通过id获取。通常用于查看管理员信息
async fn get_admin(
        &self,
        request: tonic::Request<GetAdminRequest>,
    ) -> Result<tonic::Response<GetAdminReply>, tonic::Status> {
        let GetAdminRequest { condition } = request.into_inner();
        let condition = match condition {
            Some(condition) => condition,
            None => return Err(tonic::Status::invalid_argument("请指定条件")),
        };
        let reply = match condition {
            blog_proto::get_admin_request::Condition::ByAuth(ba) => {
                let row = sqlx::query("SELECT id,email,is_del,password FROM admins WHERE email=$1")
                    .bind(ba.email)
                    .fetch_optional(&*self.pool)
                    .await
                    .map_err(|err| tonic::Status::internal(err.to_string()))?;
                if let Some(row) = row {
                    let hashed_pwd: String = row.get("password");
                    let is_verify = password::verify(&ba.password, &hashed_pwd)
                        .map_err(tonic::Status::internal)?;
                    if !is_verify {
                        return Err(tonic::Status::invalid_argument("用户名/密码错误2"));
                    } else {
                        GetAdminReply {
                            admin: Some(blog_proto::Admin {
                                id: row.get("id"),
                                email: row.get("email"),
                                password: None,
                                is_del: row.get("is_del"),
                            }),
                        }
                    }
                } else {
                    return Err(tonic::Status::invalid_argument("用户名/密码错误"));
                }
            }
            blog_proto::get_admin_request::Condition::ById(bi) => {
                let row = match bi.is_del {
                    Some(is_del) => {
                        sqlx::query("SELECT id,email,is_del FROM admins WHERE id=$1 AND is_del=$2")
                            .bind(bi.id)
                            .bind(is_del)
                    }
                    None => {
                        sqlx::query("SELECT id,email,is_del FROM admins WHERE id=$1").bind(bi.id)
                    }
                }
                .fetch_optional(&*self.pool)
                .await
                .map_err(|err| tonic::Status::internal(err.to_string()))?;
                if let Some(row) = row {
                    GetAdminReply {
                        admin: Some(blog_proto::Admin {
                            id: row.get("id"),
                            email: row.get("email"),
                            password: None,
                            is_del: row.get("is_del"),
                        }),
                    }
                } else {
                    return Err(tonic::Status::not_found("不存在的用户"));
                }
            }
        };
        Ok(tonic::Response::new(reply))
    }

我们重点来看通过email和密码来获取:

blog_proto::get_admin_request::Condition::ByAuth(ba) => {
                let row = sqlx::query("SELECT id,email,is_del,password FROM admins WHERE email=$1")
                    .bind(ba.email)
                    .fetch_optional(&*self.pool)
                    .await
                    .map_err(|err| tonic::Status::internal(err.to_string()))?;
                if let Some(row) = row {
                    let hashed_pwd: String = row.get("password");
                    let is_verify = password::verify(&ba.password, &hashed_pwd)
                        .map_err(tonic::Status::internal)?;
                    if !is_verify {
                        return Err(tonic::Status::invalid_argument("用户名/密码错误2"));
                    } else {
                        GetAdminReply {
                            admin: Some(blog_proto::Admin {
                                id: row.get("id"),
                                email: row.get("email"),
                                password: None,
                                is_del: row.get("is_del"),
                            }),
                        }
                    }
                } else {
                    return Err(tonic::Status::invalid_argument("用户名/密码错误"));
                }
            }
  • 通过email从数据库中查找记录
  • 如果有记录,通过 password::verify判断密码是否正确
  • 如果密码正确返回结果

作业:

在上面的代码中,无论管理员是否被删除,都可以进行登录,请修改代码,实现只有未删除的管理员才能登录。

修改密码 edit_admin 的实现

    async fn edit_admin(
        &self,
        request: tonic::Request<EditAdminRequest>,
    ) -> Result<tonic::Response<EditAdminReply>, tonic::Status> {
        let EditAdminRequest {
            id,
            email,
            password,
            new_password,
        } = request.into_inner();
        let new_password = match new_password {
            Some(n) => n,
            None => return Err(tonic::Status::invalid_argument("请设定新密码")),
        };
        let row = sqlx::query("SELECT password FROM admins WHERE id=$1 AND email=$2")
            .bind(id)
            .bind(&email)
            .fetch_optional(&*self.pool)
            .await
            .map_err(|err| tonic::Status::internal(err.to_string()))?;
        let pwd_in_db: String = match row {
            Some(row) => row.get("password"),
            None => return Err(tonic::Status::not_found("不存在的用户")),
        };
        let is_verify = password::verify(&password, &pwd_in_db).map_err(tonic::Status::internal)?;
        if !is_verify {
            return Err(tonic::Status::invalid_argument("密码错误"));
        }
        let hashed_new_pwd = password::hash(&new_password).map_err(tonic::Status::internal)?;
        let rows_affected = sqlx::query("UPDATE admins SET password=$1 WHERE id=$2 AND email=$3")
            .bind(hashed_new_pwd)
            .bind(id)
            .bind(&email)
            .execute(&*self.pool)
            .await
            .map_err(|err| tonic::Status::internal(err.to_string()))?
            .rows_affected();
        Ok(tonic::Response::new(EditAdminReply {
            id: id,
            ok: rows_affected > 0,
        }))
    }

管理员列表 list_admin 的实现

    async fn list_admin(
        &self,
        request: tonic::Request<ListAdminRequest>,
    ) -> Result<tonic::Response<ListAdminReply>, tonic::Status> {
        let ListAdminRequest { email, is_del } = request.into_inner();
        let rows = sqlx::query(
            r#"
            SELECT
                id,email,is_del 
            FROM
                admins
            WHERE 1=1
                AND ($1::text IS NULL OR email ILIKE CONCAT('%',$1::text,'%'))
                AND ($2::boolean IS NULL OR is_del=$2::boolean)
        "#,
        )
        .bind(email)
        .bind(is_del)
        .fetch_all(&*self.pool)
        .await
        .map_err(|err| tonic::Status::internal(err.to_string()))?;
        let mut admins = Vec::with_capacity(rows.len());
        for row in rows {
            let a = blog_proto::Admin {
                id: row.get("id"),
                email: row.get("email"),
                is_del: row.get("is_del"),
                password: None,
            };
            admins.push(a);
        }
        Ok(tonic::Response::new(ListAdminReply { admins }))
    }

创建管理员 create_admin 的实现

   async fn create_admin(
        &self,
        request: tonic::Request<CreateAdminRequest>,
    ) -> Result<tonic::Response<CreateAdminReply>, tonic::Status> {
        let request = request.into_inner();
        let AdminExistsReply { exists } = self
            .admin_exists(tonic::Request::new(AdminExistsRequest {
                condition: Some(blog_proto::admin_exists_request::Condition::Email(
                    request.email.clone(),
                )),
            }))
            .await?
            .into_inner();
        if exists {
            return Err(tonic::Status::already_exists("管理员已存在"));
        }
        let pwd = password::hash(&request.password).map_err(tonic::Status::internal)?;
        let row = sqlx::query("INSERT INTO admins (email,password) VALUES ($1,$2) RETURNING id")
            .bind(request.email)
            .bind(pwd)
            .fetch_one(&*self.pool)
            .await
            .map_err(|err| tonic::Status::internal(err.to_string()))?;
        Ok(tonic::Response::new(CreateAdminReply { id: row.get(0) }))
    }

删除/恢复管理员 toggle_admin 的实现

    async fn toggle_admin(
        &self,
        request: tonic::Request<ToggleAdminRequest>,
    ) -> Result<tonic::Response<ToggleAdminReply>, tonic::Status> {
        let ToggleAdminRequest { id } = request.into_inner();
        let row = sqlx::query("UPDATE admins SET is_del=(NOT is_del) WHERE id=$1 RETURNING is_del")
            .bind(id)
            .fetch_one(&*self.pool)
            .await
            .map_err(|err| tonic::Status::internal(err.to_string()))?;
        Ok(tonic::Response::new(ToggleAdminReply {
            id: id,
            is_del: row.get(0),
        }))
    }

运行和测试

运行、测试方法和分类服务、文章服务相似,请参考之前的章节

本章代码位于05/实现管理员服务分支。