about summary refs log tree commit diff
path: root/corp/rih/backend/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'corp/rih/backend/src/main.rs')
-rw-r--r--corp/rih/backend/src/main.rs168
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()
+        }
+    });
+}