use anyhow::{bail, Context, Result}; use log::{debug, error, info, warn, LevelFilter}; use rouille::{Request, Response}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; use std::env; use std::net::SocketAddr; use std::time::{SystemTime, UNIX_EPOCH}; use uuid::Uuid; mod yandex_log; /// Represents the request sent by the frontend application. #[derive(Debug, Deserialize)] struct FrontendReq { captcha_token: String, record: Record, } /// Represents a single record as filled in by a user. This is the /// primary data structure we want to populate and persist somewhere. #[derive(Debug, Deserialize, Serialize)] struct Record { // Record-specific metadata uuid: Uuid, // Personal information name: String, email: String, citizenship: String, // TODO personal_details: String, // Job information position: String, technologies: BTreeSet, job_details: String, work_background: String, } impl Record { fn validate(&self) -> bool { true } } fn validate_captcha(token: &str) -> Result<()> { // TODO(tazjin): pass `ip` parameter let url = "https://smartcaptcha.yandexcloud.net/validate"; let backend_key = env::var("YANDEX_SMARTCAPTCHA_KEY").context("captcha verification key not provided")?; #[derive(Deserialize)] struct CaptchaResponse { status: String, message: String, } let response: CaptchaResponse = attohttpc::get(url) .param("secret", backend_key) .param("token", token) .send() .context("failed to send captcha verification request")? .error_for_status() .context("captcha verification request failed")? .json() .context("failed to deserialize captcha verification response")?; if response.status != "ok" { warn!( "invalid captcha: {} ({})", response.message, response.status ); } info!("captcha token was valid"); Ok(()) } fn persist_record(ip: &SocketAddr, record: &Record) -> Result<()> { let bucket_name = "rih-backend-data"; let credentials = s3::creds::Credentials::from_env().context("failed to initialise storage credentials")?; let yandex_region: s3::Region = s3::Region::Custom { region: "ru-central1".to_string(), endpoint: "storage.yandexcloud.net".to_string(), }; let bucket = s3::Bucket::new(bucket_name, yandex_region, credentials) .context("failed to initialise storage client")?; let path_uuid = Uuid::new_v4(); let epoch = SystemTime::now() .duration_since(UNIX_EPOCH) .context("failed to get current time")? .as_secs(); let path = format!("/records/{}-{}.json", epoch, path_uuid); info!("writing record to '{}'", path); let data = serde_json::json!({ "ip": ip.to_string(), "record": record, }); let response = bucket .put_object(path, data.to_string().as_bytes()) .context("failed to persist storage object")?; debug!( "Object Storage response: ({}) {}", response.status_code(), response.as_str().unwrap_or("") ); Ok(()) } fn handle_submit(req: &Request) -> Result { let submitted: FrontendReq = rouille::input::json::json_input(req).context("failed to deserialise frontend request")?; validate_captcha(&submitted.captcha_token)?; if !submitted.record.validate() { bail!("invalid record: {:?}", submitted.record); } persist_record(req.remote_addr(), &submitted.record).context("failed to persist record")?; Ok(Response::text("success")) } fn main() -> Result<()> { log::set_logger(&yandex_log::YANDEX_CLOUD_LOGGER) .map(|()| log::set_max_level(LevelFilter::Info)) .expect("log configuration must succeed"); let port = env::var("PORT").unwrap_or_else(|_| /* rihb = */ "7442".to_string()); let listen = format!("0.0.0.0:{port}"); info!("launching rih-backend on: {}", listen); rouille::start_server(&listen, move |request| { if request.method() == "POST" && request.url() == "/submit" { info!("handling submit request from {}", request.remote_addr()); match handle_submit(request) { Ok(response) => { info!("submit handled successfully"); response } Err(err) => { error!("failed to handle submit: {}", err); Response::empty_400() } } } else { warn!( "no matching route for request: {} {}", request.method(), request.url() ); Response::empty_404() } }); }