实现后台管理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()函数也需要增加对其的初始化


#[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=")))
}

重定向

/// 重定向
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 分类列表

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

作业

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

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