diff --git a/src/lib.rs b/src/lib.rs index b59952a..0db616f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,17 +1,12 @@ use cfg_if::cfg_if; -use log::Level; // Add this line to import the Level type +use log::Level; use reqwest::{ header::{HeaderMap, HeaderValue, CONTENT_TYPE}, Client, }; use serde::Deserialize; -use serde_json::json; -use serde_json::Value; -use serde_qs; -use std::hash::{DefaultHasher, Hash, Hasher}; -use std::{fmt, hash}; -use totp_rs::{Algorithm, Secret, TOTP}; -use wasm_timer::UNIX_EPOCH; +use serde_json::{json, Value}; +use std::{fmt, hash::{DefaultHasher, Hash, Hasher}}; use worker::*; cfg_if! { @@ -29,75 +24,53 @@ pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result var.to_string(), - Err(_) => panic!("Client secret not set"), - }; + let client_secret = env + .var("SLACK_CLIENT_SECRET") + .expect("Client secret not set") + .to_string(); if req.method() == Method::Get { console_log!("GET request received"); - let auth = handle_oauth(req.url().unwrap(), &env).await; + let auth = match handle_oauth(req.url()?, &env).await { + Ok(auth) => auth, + Err(e) => return Response::error(format!("OAuth error: {}", e), 500), + }; console_log!("Fetched Bot and User Token"); - console_log!( - "Getting user identity with their user token, : {}", - &auth.authed_user.access_token - ); - let username = user_identity(&auth.authed_user.access_token).await; + let username = match user_identity(&auth.authed_user.access_token).await { + Ok(name) => name, + Err(e) => return Response::error(format!("User identity error: {}", e), 500), + }; - console_log!( - "Getting YSWS status with their Slack ID, {}", - &auth.authed_user.id - ); - let ysws_status = ysws_api(&auth).await; - - let slack_id = auth.authed_user.id.clone(); - let slack_username = username.clone(); - let ysws_status = ysws_status.to_string().clone(); - - let secret = slack_id + &slack_username + &ysws_status + &client_secret.to_string(); - - // let totp = TOTP::new( - // Algorithm::SHA256, - // 6, - // 1, - // 300, - // Secret::Raw(secret.as_bytes().to_vec()) - // .to_bytes() - // .expect("Failed to convert secret to bytes"), - // Some("hackclub-ysws-verifier".to_string()), - // slack_username.clone(), - // ) - // .unwrap(); - - // let token = totp.generate( - // wasm_timer::SystemTime::now() - // .duration_since(UNIX_EPOCH) - // .unwrap() - // .as_secs(), - // ); - let mut hasher: DefaultHasher = DefaultHasher::new(); - secret.hash(&mut hasher); - let hashed_secret = hasher.finish().to_string(); + console_log!("Getting YSWS status with Slack ID: {}", auth.authed_user.id); + let ysws_status = match ysws_api(&auth).await { + Ok(status) => status, + Err(e) => return Response::error(format!("YSWS API error: {}", e), 500), + }; - // Redirecting to the form with the slack_id and eligibility status + let secret = format!( + "{}{}{}{}", + auth.authed_user.id, username, ysws_status, client_secret + ); + let hashed_secret = hash_secret(&secret); - let mut url = Url::parse("https://forms.hackclub.com/t/9yNy4WYtrZus").unwrap(); // fillout form URL - url.query_pairs_mut() - .append_pair("secret", &hashed_secret.to_string()); + let mut url = Url::parse("https://forms.hackclub.com/t/9yNy4WYtrZus").unwrap(); url.query_pairs_mut() - .append_pair("slack_id", &auth.authed_user.id); - url.query_pairs_mut() - .append_pair("eligibility", &ysws_status.to_string()); - url.query_pairs_mut().append_pair("slack_user", &username); + .append_pair("secret", &hashed_secret) + .append_pair("slack_id", &auth.authed_user.id) + .append_pair("eligibility", &ysws_status.to_string()) + .append_pair("slack_user", &username); console_log!("Redirecting to {}", url); - let records = get_records(&env).await; + let records = match get_records(&env).await { + Ok(records) => records, + Err(e) => return Response::error(format!("Error fetching records: {}", e), 500), + }; console_log!("Records fetched"); - match records.is_empty() { - true => console_log!("No records to verify"), - false => verify_otp(records, env).await, + + if !records.is_empty() { + verify_hash(records, env).await } Response::redirect_with_status(url, 302) @@ -106,54 +79,55 @@ pub async fn main(req: Request, env: Env, _ctx: worker::Context) -> Result OAuthResponse { - let params: QueryParams = match serde_qs::from_str(url.query().unwrap()) { - Ok(params) => params, - Err(_) => panic!("Error parsing query parameters"), - }; - - // Retrieve environment variables - let client_id = match env.var("SLACK_CLIENT_ID") { - Ok(var) => var.to_string(), - Err(_) => panic!("Client ID not set"), - }; - let client_secret = match env.var("SLACK_CLIENT_SECRET") { - Ok(var) => var.to_string(), - Err(_) => panic!("Client secret not set"), - }; - let redirect_uri = match env.var("SLACK_REDIRECT_URI") { - Ok(var) => var.to_string(), - Err(_) => panic!("Redirect URI not set"), - }; +// Handle the OAuth flow to get the access token/Slack ID +async fn handle_oauth(url: Url, env: &Env) -> Result { + let params: QueryParams = serde_qs::from_str(url.query().ok_or("Missing query params")?) + .map_err(|_| "Error parsing query parameters".to_string())?; + + let client_id = env + .var("SLACK_CLIENT_ID") + .map_err(|_| "Client ID not set".to_string())? + .to_string(); + let client_secret = env + .var("SLACK_CLIENT_SECRET") + .map_err(|_| "Client secret not set".to_string())? + .to_string(); + let redirect_uri = env + .var("SLACK_REDIRECT_URI") + .map_err(|_| "Redirect URI not set".to_string())? + .to_string(); console_log!("Client ID: {}", client_id); console_log!("Code: {}", params.code); - // Exchange authorization code for an access token console_log!("Exchange code for token response..."); - let access_token_response = - exchange_code_for_token(&client_id, &client_secret, ¶ms.code, &redirect_uri).await; - return access_token_response; + exchange_code_for_token(&client_id, &client_secret, ¶ms.code, &redirect_uri).await +} + +// Hashing secret using DefaultHasher +fn hash_secret(secret: &str) -> String { + let mut hasher = DefaultHasher::new(); + secret.hash(&mut hasher); + hasher.finish().to_string() } #[derive(Deserialize)] -pub struct QueryParams { - pub code: String, - pub state: Option, +struct QueryParams { + code: String, + state: Option, } #[derive(Deserialize, Debug)] -pub struct OAuthResponse { - pub ok: bool, - pub access_token: String, // bot access token - pub authed_user: User, +struct OAuthResponse { + ok: bool, + access_token: String, // bot access token + authed_user: User, } #[derive(Deserialize, Debug)] -pub struct User { - pub id: String, - pub access_token: String, // user auth token +struct User { + id: String, + access_token: String, // user auth token } pub enum YSWSStatus { @@ -180,81 +154,87 @@ impl fmt::Display for YSWSStatus { } } -pub async fn exchange_code_for_token( +async fn exchange_code_for_token( client_id: &str, client_secret: &str, code: &str, redirect_uri: &str, -) -> OAuthResponse { - let client: Client = Client::new(); - - let request = client.post("https://slack.com/api/oauth.v2.access").form(&[ - ("client_id", client_id), - ("client_secret", client_secret), - ("code", code), - ("redirect_uri", redirect_uri), - ]); - - let response = request.send().await.unwrap(); - let oauth_response: OAuthResponse = response.json::().await.unwrap(); - oauth_response +) -> Result { + let client = Client::new(); + let response = client + .post("https://slack.com/api/oauth.v2.access") + .form(&[ + ("client_id", client_id), + ("client_secret", client_secret), + ("code", code), + ("redirect_uri", redirect_uri), + ]) + .send() + .await + .map_err(|e| format!("Request error: {}", e))?; + + if response.status().is_success() { + Ok(response + .json::() + .await + .map_err(|e| format!("Parsing error: {}", e))?) + } else { + Err(format!( + "OAuth request failed with status: {}", + response.status() + ).into()) + } } -async fn ysws_api(user: &OAuthResponse) -> YSWSStatus { +async fn ysws_api(user: &OAuthResponse) -> Result { let client = Client::new(); let url = "https://verify.hackclub.dev/api/status"; - let json_body: Value = serde_json::json!({ - "slack_id": user.authed_user.id, - }); + let json_body = json!({ "slack_id": user.authed_user.id }); - // Create headers let mut headers = HeaderMap::new(); headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json")); - // Send the POST request let response = client .post(url) .headers(headers) .json(&json_body) .send() .await - .unwrap(); - - let response_text = response.text().await.unwrap(); - - if response_text.contains("Eligible L1") { - YSWSStatus::EligibleL1 - } else if response_text.contains("Eligible L2") { - YSWSStatus::EligibleL2 - } else if response_text.contains("Ineligible") { - YSWSStatus::Ineligible - } else if response_text.contains("Insufficient") { - YSWSStatus::Insufficient - } else if response_text.contains("Sanctioned Country") { - YSWSStatus::SanctionedCountry - } else if response_text.contains("Testing") { - YSWSStatus::Testing - } else { - YSWSStatus::Unknown - } + .map_err(|e| format!("Request error: {}", e))?; + + let response_text = response + .text() + .await + .map_err(|e| format!("Text error: {}", e))?; + Ok(match response_text.as_str() { + "Eligible L1" => YSWSStatus::EligibleL1, + "Eligible L2" => YSWSStatus::EligibleL2, + "Ineligible" => YSWSStatus::Ineligible, + "Insufficient" => YSWSStatus::Insufficient, + "Sanctioned Country" => YSWSStatus::SanctionedCountry, + "Testing" => YSWSStatus::Testing, + _ => YSWSStatus::Unknown, + }) } -async fn user_identity(access_token: &String) -> String { +async fn user_identity(access_token: &str) -> Result { let client = Client::new(); let url = "https://slack.com/api/openid.connect.userInfo"; - console_log!("Getting user info with access token: {}", access_token); let response = client .get(url) .bearer_auth(access_token) .send() .await - .unwrap(); - // Parse the response as JSON - let user_info: UserInfo = response.json::().await.unwrap(); + .map_err(|e| format!("Request error: {}", e))?; - user_info.name + let user_info: UserInfo = response + .json() + .await + .map_err(|e| format!("Parsing error: {}", e))?; + + Ok(user_info.name) } #[derive(Deserialize, Debug)] @@ -264,40 +244,61 @@ struct UserInfo { email: String, } -async fn get_records(env: &Env) -> Vec { +async fn get_records(env: &Env) -> Result> { let client = Client::new(); let url = "http://hackclub-ysws-api.jasperworkers.workers.dev/submissions"; - let jasper_api = match env.var("JASPER_API") { - Ok(var) => var.to_string(), - Err(_) => panic!("Jasper API not set"), - }; + let jasper_api = env + .var("JASPER_API") + .map_err(|_| "Jasper API not set".to_string())? + .to_string(); let response = client .get(url) .bearer_auth(jasper_api) .send() .await - .unwrap(); + .map_err(|e| format!("Request error: {}", e))?; - let response_values: Vec = response.json::>().await.unwrap(); - let mut records: Vec = Vec::new(); + let response_values: Vec = response + .json() + .await + .map_err(|e| format!("Parsing error: {}", e))?; + let mut records = Vec::new(); for value in response_values { match serde_json::from_value::(value.clone()) { - Ok(record) => { - records.push(record); - } + Ok(record) => records.push(record), Err(e) => { - console_log!("Skipping invalid record: {:?}", value); - console_log!("Error: {:?}", e); + console_log!("Skipping invalid record: {}", e); } } } - records + + Ok(records) +} + +#[derive(Deserialize, Debug)] +struct Record { + id: String, + #[serde(rename = "createdTime")] + created_time: String, + fields: Fields, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "PascalCase")] +struct Fields { + eligibility: String, + #[serde(rename = "OTP")] + otp: String, + #[serde(rename = "Slack ID")] + slack_id: String, + #[serde(rename = "SlackUsername")] + slack_username: String, } -async fn verify_otp(records: Vec, env: Env) { +async fn verify_hash(records: Vec, env: Env) { console_log!("Looking into {} records", records.len()); for record in records { @@ -370,23 +371,3 @@ async fn verify_otp(records: Vec, env: Env) { } } } - -#[derive(Deserialize, Debug)] -struct Record { - id: String, - #[serde(rename = "createdTime")] - created_time: String, - fields: Fields, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "PascalCase")] -struct Fields { - eligibility: String, - #[serde(rename = "OTP")] - otp: String, - #[serde(rename = "Slack ID")] - slack_id: String, - #[serde(rename = "SlackUsername")] - slack_username: String, -}