main.rs

  1mod admin;
  2mod assets;
  3mod auth;
  4mod db;
  5mod env;
  6mod errors;
  7mod expiring;
  8mod github;
  9mod home;
 10mod rpc;
 11mod team;
 12mod story;
 13mod updates;
 14mod releases;
 15mod community;
 16
 17use self::errors::TideResultExt as _;
 18use anyhow::Result;
 19use async_std::net::TcpListener;
 20use async_trait::async_trait;
 21use auth::RequestExt as _;
 22use db::Db;
 23use handlebars::{Handlebars, TemplateRenderError};
 24use parking_lot::RwLock;
 25use rust_embed::RustEmbed;
 26use serde::{Deserialize, Serialize};
 27use std::sync::Arc;
 28use surf::http::cookies::SameSite;
 29use tide::{log, sessions::SessionMiddleware};
 30use tide_compress::CompressMiddleware;
 31use zrpc::Peer;
 32
 33type Request = tide::Request<Arc<AppState>>;
 34
 35#[derive(RustEmbed)]
 36#[folder = "templates"]
 37struct Templates;
 38
 39#[derive(Default, Deserialize)]
 40pub struct Config {
 41    pub http_port: u16,
 42    pub database_url: String,
 43    pub session_secret: String,
 44    pub github_app_id: usize,
 45    pub github_client_id: String,
 46    pub github_client_secret: String,
 47    pub github_private_key: String,
 48}
 49
 50pub struct AppState {
 51    db: Db,
 52    handlebars: RwLock<Handlebars<'static>>,
 53    auth_client: auth::Client,
 54    github_client: Arc<github::AppClient>,
 55    repo_client: github::RepoClient,
 56    config: Config,
 57}
 58
 59impl AppState {
 60    async fn new(config: Config) -> tide::Result<Arc<Self>> {
 61        let db = Db::new(&config.database_url, 5).await?;
 62        let github_client =
 63            github::AppClient::new(config.github_app_id, config.github_private_key.clone());
 64        let repo_client = github_client
 65            .repo("zed-industries/zed".into())
 66            .await
 67            .context("failed to initialize github client")?;
 68
 69        let this = Self {
 70            db,
 71            handlebars: Default::default(),
 72            auth_client: auth::build_client(&config.github_client_id, &config.github_client_secret),
 73            github_client,
 74            repo_client,
 75            config,
 76        };
 77        this.register_partials();
 78        Ok(Arc::new(this))
 79    }
 80
 81    fn register_partials(&self) {
 82        for path in Templates::iter() {
 83            if let Some(partial_name) = path
 84                .strip_prefix("partials/")
 85                .and_then(|path| path.strip_suffix(".hbs"))
 86            {
 87                let partial = Templates::get(path.as_ref()).unwrap();
 88                self.handlebars
 89                    .write()
 90                    .register_partial(partial_name, std::str::from_utf8(&partial.data).unwrap())
 91                    .unwrap()
 92            }
 93        }
 94    }
 95
 96    fn render_template(
 97        &self,
 98        path: &'static str,
 99        data: &impl Serialize,
100    ) -> Result<String, TemplateRenderError> {
101        #[cfg(debug_assertions)]
102        self.register_partials();
103
104        self.handlebars.read().render_template(
105            std::str::from_utf8(&Templates::get(path).unwrap().data).unwrap(),
106            data,
107        )
108    }
109}
110
111#[async_trait]
112trait RequestExt {
113    async fn layout_data(&mut self) -> tide::Result<Arc<LayoutData>>;
114    fn db(&self) -> &Db;
115}
116
117#[async_trait]
118impl RequestExt for Request {
119    async fn layout_data(&mut self) -> tide::Result<Arc<LayoutData>> {
120        if self.ext::<Arc<LayoutData>>().is_none() {
121            self.set_ext(Arc::new(LayoutData {
122                current_user: self.current_user().await?,
123            }));
124        }
125        Ok(self.ext::<Arc<LayoutData>>().unwrap().clone())
126    }
127
128    fn db(&self) -> &Db {
129        &self.state().db
130    }
131}
132
133#[derive(Serialize)]
134struct LayoutData {
135    current_user: Option<auth::User>,
136}
137
138#[async_std::main]
139async fn main() -> tide::Result<()> {
140    log::start();
141
142    if let Err(error) = env::load_dotenv() {
143        log::error!(
144            "error loading .env.toml (this is expected in production): {}",
145            error
146        );
147    }
148
149    let config = envy::from_env::<Config>().expect("error loading config");
150    let state = AppState::new(config).await?;
151    let rpc = Peer::new();
152    run_server(
153        state.clone(),
154        rpc,
155        TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)).await?,
156    )
157    .await?;
158    Ok(())
159}
160
161pub async fn run_server(
162    state: Arc<AppState>,
163    rpc: Arc<Peer>,
164    listener: TcpListener,
165) -> tide::Result<()> {
166    let mut web = tide::with_state(state.clone());
167    web.with(CompressMiddleware::new());
168    web.with(
169        SessionMiddleware::new(
170            db::SessionStore::new_with_table_name(&state.config.database_url, "sessions")
171                .await
172                .unwrap(),
173            state.config.session_secret.as_bytes(),
174        )
175        .with_same_site_policy(SameSite::Lax), // Required obtain our session in /auth_callback
176    );
177    web.with(errors::Middleware);
178    home::add_routes(&mut web);
179    team::add_routes(&mut web);
180    story::add_routes(&mut web);
181    releases::add_routes(&mut web);
182    updates::add_routes(&mut web);
183    community::add_routes(&mut web);
184    admin::add_routes(&mut web);
185    auth::add_routes(&mut web);
186    assets::add_routes(&mut web);
187
188    let mut app = tide::with_state(state.clone());
189    rpc::add_routes(&mut app, &rpc);
190    app.at("/").nest(web);
191
192    app.listen(listener).await?;
193
194    Ok(())
195}