域名 AXUM.RS 将于 2025 年 10 月到期。我们无意再对其进行续费,如果你有意接续这个域名,请与我们取得联系。
  • AXUM.RS 现仅需人民币 3000 元(大写:叁仟元整。接受适度议价
  • 按照行业规则,AXUM.RS 到期后,大概率会进入长时间的赎回期,该期间内,如果你想拥有该域名,将要付出高额的费用
  • 我们已启用 AXUM.EU.ORG 域名,并将持续运营
  • 仅接受微信或支付宝交易
如果你对 AXUM.RS 有兴趣,请和我们进行联系:

实现后台管理web服务

本章将使用 axum 调用 gRPC 服务来实现后台管理的 web 服务。

状态共享 AppState 增加管理员服务

pub struct AppState {
    pub tera: Tera,
    pub cate: CategoryServiceClient<tonic::transport::Channel>,
    pub topic: TopicServiceClient<tonic::transport::Channel>,
    pub admin: AdminServiceClient<tonic::transport::Channel>,
}

后台管理的 AppState 增加了管理员服务的连接。相应地,main()函数也需要增加对其的初始化

登录状态和Cookie

为了维护登录状态,我们使用 Cookie

handler::login 登录

pub async fn login(
    Extension(state): Extension<Arc<AppState>>,
    Form(frm): Form<form::Login>,
) -> Result<(StatusCode, HeaderMap), String> {
    let condition = blog_proto::get_admin_request::Condition::ByAuth(ByAuth {
        email: frm.email,
        password: frm.password,
    });
    let mut admin = state.admin.clone();
    let resp = admin
        .get_admin(tonic::Request::new(blog_proto::GetAdminRequest {
            condition: Some(condition),
        }))
        .await
        .map_err(|err| err.to_string())?;
    let repl = resp.into_inner();
    let logined_admin = match repl.admin {
        Some(la) => la,
        None => return Err("登录失败".to_string()),
    };
    let cookie = format!("axum_rs_token={}", &logined_admin.email);
    Ok(redirect_with_cookie("/m/cate", Some(&cookie)))
}

handler::logout 注销登录

重定向

这两个函数取自axum-rs代码

/// 重定向
pub fn redirect(url: &str) -> (StatusCode, HeaderMap) {
    redirect_with_cookie(url, None)
}

/// 重定向
pub fn redirect_with_cookie(url: &str, cookie: Option<&str>) -> (StatusCode, HeaderMap) {
    let mut header = HeaderMap::new();
    header.insert(axum::http::header::LOCATION, url.parse().unwrap());
    if let Some(cookie) = cookie {
        header.insert(axum::http::header::SET_COOKIE, cookie.parse().unwrap());
    }
    (StatusCode::FOUND, header)
}

handler::list_cate 分类列表

pub async fn list_cate(
    Extension(state): Extension<Arc<AppState>>,
    Query(params): Query<form::CateListFilter>,
) -> Result<Html<String>, String> {
    let mut ctx = Context::new();
    let msg = params.msg.clone();
    if let Some(msg) = msg {
        ctx.insert("msg", &msg);
    }
    let mut cate = state.cate.clone();
    let resp = cate.list_category(tonic::Request::new(params.into())).await;
    let reply = match resp {
        Ok(r) => r.into_inner(),
        Err(err) => {
            if err.code() == tonic::Code::NotFound {
                ListCategoryReply { categories: vec![] }
            } else {
                return Err(err.to_string());
            }
        }
    };

    let mut cate_list = Vec::with_capacity(reply.categories.len());
    for c in reply.categories {
        let tc: blog_types::Category = c.into();
        cate_list.push(tc);
    }
    ctx.insert("cate_list", &cate_list);

    let out = state
        .tera
        .render("cate/index.html", &ctx)
        .map_err(|err| err.to_string())?;
    Ok(Html(out))
}
pub async fn add_cate(
    Extension(state): Extension<Arc<AppState>>,
    Form(frm): Form<form::AddCatetory>,
) -> Result<(StatusCode, HeaderMap), String> {
    let mut cate = state.cate.clone();
    let resp = cate
        .create_category(tonic::Request::new(blog_proto::CreateCategoryRequest {
            name: frm.name,
        }))
        .await
        .map_err(|err| err.to_string())?;
    let repl = resp.into_inner();
    let url = format!("/m/cate?msg=分类(ID为{})添加成功", repl.id);
    Ok(redirect(&url))
}

作业

本章的代码只实现登录、注册登录、分类列表和添加分类。请结合之前章节和源码里的导航菜单,将其余功能实现完整。

本章代码位于06/后台管理分支。

要查看完整内容,请先登录