diff options
Diffstat (limited to 'corp/rih/backend/src/main.rs')
-rw-r--r-- | corp/rih/backend/src/main.rs | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/corp/rih/backend/src/main.rs b/corp/rih/backend/src/main.rs new file mode 100644 index 0000000000..208e0367c6 --- /dev/null +++ b/corp/rih/backend/src/main.rs @@ -0,0 +1,168 @@ +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<String>, + 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("<unprintable>") + ); + + Ok(()) +} + +fn handle_submit(req: &Request) -> Result<Response> { + 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() + } + }); +} |