1mod api;
2mod assets;
3mod auth;
4mod db;
5mod env;
6mod errors;
7mod expiring;
8mod github;
9mod rpc;
10
11use ::rpc::Peer;
12use async_std::net::TcpListener;
13use async_trait::async_trait;
14use db::{Db, PostgresDb};
15use handlebars::Handlebars;
16use parking_lot::RwLock;
17use rust_embed::RustEmbed;
18use serde::Deserialize;
19use std::sync::Arc;
20use surf::http::cookies::SameSite;
21use tide::sessions::SessionMiddleware;
22use tide_compress::CompressMiddleware;
23
24type Request = tide::Request<Arc<AppState>>;
25
26#[derive(RustEmbed)]
27#[folder = "templates"]
28struct Templates;
29
30#[derive(Default, Deserialize)]
31pub struct Config {
32 pub http_port: u16,
33 pub database_url: String,
34 pub session_secret: String,
35 pub github_app_id: usize,
36 pub github_client_id: String,
37 pub github_client_secret: String,
38 pub github_private_key: String,
39 pub api_token: String,
40}
41
42pub struct AppState {
43 db: Arc<dyn Db>,
44 handlebars: RwLock<Handlebars<'static>>,
45 config: Config,
46}
47
48impl AppState {
49 async fn new(config: Config) -> tide::Result<Arc<Self>> {
50 let db = PostgresDb::new(&config.database_url, 5).await?;
51
52 let this = Self {
53 db: Arc::new(db),
54 handlebars: Default::default(),
55 config,
56 };
57 this.register_partials();
58 Ok(Arc::new(this))
59 }
60
61 fn register_partials(&self) {
62 for path in Templates::iter() {
63 if let Some(partial_name) = path
64 .strip_prefix("partials/")
65 .and_then(|path| path.strip_suffix(".hbs"))
66 {
67 let partial = Templates::get(path.as_ref()).unwrap();
68 self.handlebars
69 .write()
70 .register_partial(partial_name, std::str::from_utf8(&partial.data).unwrap())
71 .unwrap()
72 }
73 }
74 }
75}
76
77#[async_trait]
78trait RequestExt {
79 fn db(&self) -> &Arc<dyn Db>;
80}
81
82#[async_trait]
83impl RequestExt for Request {
84 fn db(&self) -> &Arc<dyn Db> {
85 &self.state().db
86 }
87}
88
89#[async_std::main]
90async fn main() -> tide::Result<()> {
91 if std::env::var("LOG_JSON").is_ok() {
92 json_env_logger::init();
93 } else {
94 tide::log::start();
95 }
96
97 if let Err(error) = env::load_dotenv() {
98 log::error!(
99 "error loading .env.toml (this is expected in production): {}",
100 error
101 );
102 }
103
104 let config = envy::from_env::<Config>().expect("error loading config");
105 let state = AppState::new(config).await?;
106 let rpc = Peer::new();
107 run_server(
108 state.clone(),
109 rpc,
110 TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)).await?,
111 )
112 .await?;
113 Ok(())
114}
115
116pub async fn run_server(
117 state: Arc<AppState>,
118 rpc: Arc<Peer>,
119 listener: TcpListener,
120) -> tide::Result<()> {
121 let mut web = tide::with_state(state.clone());
122 web.with(CompressMiddleware::new());
123 web.with(
124 SessionMiddleware::new(
125 db::SessionStore::new_with_table_name(&state.config.database_url, "sessions")
126 .await
127 .unwrap(),
128 state.config.session_secret.as_bytes(),
129 )
130 .with_same_site_policy(SameSite::Lax), // Required obtain our session in /auth_callback
131 );
132 api::add_routes(&mut web);
133
134 let mut assets = tide::new();
135 assets.with(CompressMiddleware::new());
136 assets::add_routes(&mut assets);
137
138 let mut app = tide::with_state(state.clone());
139 rpc::add_routes(&mut app, &rpc);
140
141 app.at("/").nest(web);
142 app.at("/static").nest(assets);
143
144 app.listen(listener).await?;
145
146 Ok(())
147}