main.rs

  1mod admin;
  2mod api;
  3mod assets;
  4mod auth;
  5mod community;
  6mod db;
  7mod env;
  8mod errors;
  9mod expiring;
 10mod github;
 11mod home;
 12mod releases;
 13mod rpc;
 14mod team;
 15
 16use self::errors::TideResultExt as _;
 17use ::rpc::Peer;
 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;
 31
 32type Request = tide::Request<Arc<AppState>>;
 33
 34#[derive(RustEmbed)]
 35#[folder = "templates"]
 36struct Templates;
 37
 38#[derive(Default, Deserialize)]
 39pub struct Config {
 40    pub http_port: u16,
 41    pub database_url: String,
 42    pub session_secret: String,
 43    pub github_app_id: usize,
 44    pub github_client_id: String,
 45    pub github_client_secret: String,
 46    pub github_private_key: String,
 47    pub api_token: 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    api::add_routes(&mut web);
179    home::add_routes(&mut web);
180    team::add_routes(&mut web);
181    releases::add_routes(&mut web);
182    community::add_routes(&mut web);
183    admin::add_routes(&mut web);
184    auth::add_routes(&mut web);
185
186    let mut assets = tide::new();
187    assets.with(CompressMiddleware::new());
188    assets::add_routes(&mut assets);
189
190    let mut app = tide::with_state(state.clone());
191    rpc::add_routes(&mut app, &rpc);
192
193    app.at("/").nest(web);
194    app.at("/static").nest(assets);
195
196    app.listen(listener).await?;
197
198    Ok(())
199}