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