#[macro_use] extern crate rocket; use core::panic; use std::collections::HashMap; use std::convert::Infallible; use std::env; use rocket::fs::{FileServer, relative}; use rocket::{Request, request}; use rocket::http::{Cookie, CookieJar}; use rocket::request::{FromRequest, FlashMessage}; use rocket::response::{Redirect, Flash}; use rocket_dyn_templates::{context, Template}; use std::fs; use sass_rocket_fairing::SassFairing; mod challenge; use challenge::Challenge; mod kyujitai; #[get("/")] fn get_challenge(challenge: u32, cookies: &CookieJar<'_>) -> Template { let value = cookies .get_private(TOKEN_COOKIE) .map(|cookie| cookie.value().to_owned()); let logged_in = value.is_some(); Template::render( "index", context! { challenge, logged_in, content: { use comrak::{parse_document, Arena, ComrakOptions}; let options = { let mut options = ComrakOptions::default(); options.extension.front_matter_delimiter = Some("---".to_owned()); options }; let arena = Arena::new(); let root = parse_document( &arena, &fs::read_to_string(format!("content/challenges/{challenge}.md")).expect("Couldn't find challenge file"), &options, ); if let Some(node) = root.children().next() { if let comrak::nodes::NodeValue::FrontMatter(frontmatter) = &node.data.borrow().value { let frontmatter = { // Trim starting and ending fences let lines: Vec<&str> = frontmatter.trim().lines().collect(); lines[1..lines.len() - 1].join("\n") }; let challenge: Challenge = serde_yaml::from_str(&frontmatter).unwrap(); challenge } else { panic!("No frontmatter!") } } else { panic!("Empty document!") } } }, ) } #[get("/login")] fn login() -> Redirect { Redirect::to(format!( "https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code&scope=identify%20guilds.join%20guilds", client_id = env::var("CLIENT_ID").unwrap(), redirect_uri = format!("{}login", env::var("DOMAIN").unwrap()), )) // TODO: After returning from Discord go to previous page (with Referer?) } #[get("/login?")] fn login_success(code: String, cookies: &CookieJar<'_>) -> Redirect { cookies.add_private(Cookie::new(TOKEN_COOKIE, code)); Redirect::to("/") } struct Referer(Option); #[rocket::async_trait] impl<'r> FromRequest<'r> for Referer { type Error = Infallible; async fn from_request(req: &'r Request<'_>) -> request::Outcome { let referer = req.headers().get_one("Referer"); request::Outcome::Success(Referer(referer.map(|referer| referer.to_owned()))) } } const TOKEN_COOKIE: &str = "token"; #[get("/logout")] fn logout(cookies: &CookieJar<'_>, referer: Referer) -> Redirect { let token = match cookies.get_private(TOKEN_COOKIE) { Some(cookie) => cookie.value().to_owned(), None => return Redirect::to("/"), }; rocket::tokio::spawn(async { let client = reqwest::Client::new(); let params = { let mut params = HashMap::new(); params.insert("client_id", env::var("CLIENT_ID").unwrap()); params.insert("client_secret", env::var("CLIENT_SECRET").unwrap()); params.insert("token", token); params }; match client.post("https://discord.com/api/oauth2/token/revoke") .header("Content-Type", "application/x-www-form-urlencoded") .form(¶ms) .send().await { Ok(_) => println!("Successfully revoked token"), Err(error) => println!("Failed to revoke token: {:?}", error), }; }); cookies.remove_private(Cookie::named(TOKEN_COOKIE)); let redirect_url = referer.0.unwrap_or("/".to_owned()); Redirect::to(redirect_url) } #[launch] fn rocket() -> _ { let config = rocket::Config::figment().merge(("port", 1313)); dotenv::dotenv().expect("Failed to load .env file"); rocket::custom(config) .mount("/", routes![get_challenge, login, login_success, logout]) .mount("/css", FileServer::from(relative!("styles/css"))) .attach(Template::fairing()) .attach(SassFairing::default()) }