实现后台管理web服务

70887
2022/09/24 06:32:44

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

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

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

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

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代码

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))
}

handler::add_cate 添加分类

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/后台管理分支。