域名 AXUM.RS 将于2025年10月到期。我们无意再对其进行续费,我们希望你能够接续这个域名,让更多 AXUM 开发者继续受益。
  • 方案1️⃣AXUM.RS 域名 = 3000
  • 方案2️⃣方案1️⃣ + 本站所有专题原始 Markdown 文档 = 5000
  • 方案3️⃣方案2️⃣ + 本站原始数据库 = 5500
如果你有意接续这份 AXUM 情怀,请与我们取得联系。
说明:
  1. 如果有人购买 AXUM.RS 域名(方案1️⃣),或者该域名到期,本站将启用新的免费域名继续提供服务。
  2. 如果有人购买了 AXUM.RS 域名,且同时购买了内容和/或数据库(方案2️⃣/方案3️⃣),本站将关闭。届时我们或许会以另一种方式与你再相遇。

实现后台管理web服务

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

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()函数也需要增加对其的初始化


#[tokio::main]
async fn main() {
    let addr = "0.0.0.0:59527";

    let cate = CategoryServiceClient::connect("http://127.0.0.1:19527")
        .await
        .unwrap();
    let topic = TopicServiceClient::connect("http://127.0.0.1:29527")
        .await
        .unwrap();
    let admin = AdminServiceClient::connect("http://127.0.0.1:49527")
        .await
        .unwrap();
    let tera = Tera::new("blog-backend/templates/**/*.html").unwrap();

    let m_router = Router::new().route("/cate", get(handler::list_cate)).route(
        "/cate/add",
        get(handler::add_cate_ui).post(handler::add_cate),
    );

    let app = Router::new()
        .nest("/m", m_router)
        .route("/", get(handler::index))
        .route("/login", get(handler::login_ui).post(handler::login))
        .route("/logout", get(handler::logout))
        .layer(Extension(Arc::new(model::AppState {
            tera,
            admin,
            cate,
            topic,
        })));

    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

登录状态和Cookie

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

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 注销登录

pub async fn logout() -> Result<(StatusCode, HeaderMap), String> {
    Ok(redirect_with_cookie("/login", Some("axum_rs_token=")))
}

重定向

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

这两个函数取自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)
}
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))
}

handler::add_cate 添加分类

作业

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

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

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