main.rs

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