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