diff --git a/Cargo.toml b/Cargo.toml index d243bb93..2b613676 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,4 +25,6 @@ chrono = "0.4.19" lazy_static = "1.4.0" regex = "1.10.0" ctor = "0.2.6" -axum-client-ip = "0.4.0" \ No newline at end of file +axum-client-ip = "0.4.0" +jsonwebtoken = "9" +tower = "0.4.13" diff --git a/config.template.toml b/config.template.toml index c7ceafb6..1d6b080b 100644 --- a/config.template.toml +++ b/config.template.toml @@ -5,6 +5,10 @@ port = 8080 name = "starkship_server" connection_string = "xxxxxx" +[auth] +secret_key = "secret_key" +expiry_duration = 0 + [rhino] api_endpoint="XXXXXXXXXXXX" diff --git a/src/common/mod.rs b/src/common/mod.rs index e6ce6f50..00e03f52 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -3,4 +3,4 @@ pub mod has_deployed_time; pub mod verify_has_nft; pub mod verify_has_root_domain; pub mod verify_has_root_or_braavos_domain; -pub mod verify_quiz; +pub mod verify_quiz; \ No newline at end of file diff --git a/src/common/verify_quiz.rs b/src/common/verify_quiz.rs index 077d2516..dc1df3d6 100644 --- a/src/common/verify_quiz.rs +++ b/src/common/verify_quiz.rs @@ -1,44 +1,103 @@ -use crate::config::{Config, Quiz, QuizQuestionType}; +use futures::StreamExt; +use mongodb::bson::{doc, from_document}; +use mongodb::Database; +use crate::config::{Quiz, QuizQuestionType}; use starknet::core::types::FieldElement; +use crate::models::QuizInsertDocument; + +fn match_vectors(vector1: &Vec, vector2: &Vec) -> bool { + // Check if vectors have the same length + if vector1.len() != vector2.len() { + return false; + } + + // Check if vectors are equal element-wise + let equal = vector1 == vector2; + equal +} // addr is currently unused, this could become the case if we generate // a deterministic permutation of answers in the future. Seems non necessary for now #[allow(dead_code)] -pub fn verify_quiz( - config: &Config, +pub async fn verify_quiz( + config: &Database, _addr: FieldElement, - quiz_name: &str, + quiz_name: &i64, user_answers_list: &Vec>, ) -> bool { - let quiz: &Quiz = match config.quizzes.get(quiz_name) { - Some(quiz) => quiz, - None => return false, // Quiz not found - }; - - for (question, user_answers) in quiz.questions.iter().zip(user_answers_list.iter()) { - match question.kind { - QuizQuestionType::TextChoice | QuizQuestionType::ImageChoice => { - if let Some(correct_answers) = &question.correct_answers { - // if user_answers does not fit in correct_answers or isn't the same size - if user_answers.len() != correct_answers.len() - || !user_answers - .iter() - .all(|&item| correct_answers.contains(&item)) - { - return false; + let collection = config.collection::("quizzes"); + let pipeline = vec![ + doc! { + "$match": doc! { + "id": &quiz_name + } + }, + doc! { + "$lookup": doc! { + "from": "quiz_questions", + "let": doc! { + "id": "$id" + }, + "pipeline": [ + doc! { + "$match": doc! { + "quiz_id": &quiz_name + } + }, + doc! { + "$project": doc! { + "quiz_id": 0, + "_id": 0 + } } - } else { - return false; - } + ], + "as": "questions" + } + }, + doc! { + "$project": doc! { + "_id": 0, + "id": 0 } - QuizQuestionType::Ordering => { - if let Some(correct_order) = &question.correct_order { - if correct_order != user_answers { - return false; + }, + ]; + + let mut quiz_document = collection.aggregate(pipeline, None).await.unwrap(); + + while let Some(result) = quiz_document.next().await { + match result { + Ok(document) => { + let quiz: Quiz = from_document(document).unwrap(); + let mut correct_answers_count = 0; + for (i, user_answers) in user_answers_list.iter().enumerate() { + let question = &quiz.questions[i]; + let mut user_answers_list = user_answers.clone(); + let correct_answers: bool = match question.kind { + QuizQuestionType::TextChoice => { + let mut correct_answers = question.correct_answers.clone().unwrap(); + correct_answers.sort(); + user_answers_list.sort(); + match_vectors(&correct_answers, &user_answers) + } + QuizQuestionType::ImageChoice => { + let mut correct_answers = question.correct_answers.clone().unwrap(); + correct_answers.sort(); + user_answers_list.sort(); + match_vectors(&correct_answers, &user_answers) + } + QuizQuestionType::Ordering => { + let correct_answers = question.correct_answers.clone().unwrap(); + match_vectors(&correct_answers, &user_answers_list) + } + }; + if correct_answers { + correct_answers_count += 1; } - } else { - return false; } + return correct_answers_count == quiz.questions.len(); + } + Err(_e) => { + return false; } } } diff --git a/src/config.rs b/src/config.rs index b8dd598a..5710bd1e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,17 +124,17 @@ impl<'de> Deserialize<'de> for QuizQuestionType { } } -pub_struct!(Clone, Deserialize; QuizQuestion { +pub_struct!(Clone, Deserialize,Debug; QuizQuestion { kind: QuizQuestionType, layout: String, question: String, options: Vec, correct_answers: Option>, - correct_order: Option>, + correct_order: Option>, image_for_layout: Option, }); -pub_struct!(Clone, Deserialize; Quiz { +pub_struct!(Clone, Deserialize,Debug; Quiz { name: String, desc: String, questions: Vec, @@ -167,6 +167,11 @@ pub_struct!(Clone, Deserialize; Achievements { carbonable: Achievement, }); +pub_struct!(Clone, Deserialize; AuthSetup { + secret_key: String, + expiry_duration: i64, +}); + pub_struct!(Clone, Deserialize; Config { server: Server, database: Database, @@ -183,6 +188,7 @@ pub_struct!(Clone, Deserialize; Config { rhino: PublicApi, rango: Api, pyramid: ApiEndpoint, + auth:AuthSetup, }); pub fn load() -> Config { diff --git a/src/endpoints/admin/custom/create_custom.rs b/src/endpoints/admin/custom/create_custom.rs new file mode 100644 index 00000000..6e45a61b --- /dev/null +++ b/src/endpoints/admin/custom/create_custom.rs @@ -0,0 +1,83 @@ +use crate::models::{QuestDocument, QuestTaskDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::FindOneOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use crate::utils::verify_quest_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateCustom { + quest_id: u32, + name: String, + desc: String, + cta: String, + href: String, +}); + +#[route(post, "/admin/tasks/custom/create", crate::endpoints::admin::custom::create_custom)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &(body.quest_id as i32)).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + verify_redirect: Some(body.href.clone()), + href: body.href.clone(), + quest_id : body.quest_id, + id: next_id, + cta: body.cta.clone(), + verify_endpoint: "/quests/verify_custom".to_string(), + verify_endpoint_type: "default".to_string(), + task_type: Some("custom".to_string()), + discord_guild_id: None, + quiz_name: None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, + None, + ) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating tasks".to_string()), + }; +} diff --git a/src/endpoints/admin/custom/mod.rs b/src/endpoints/admin/custom/mod.rs new file mode 100644 index 00000000..04726dc9 --- /dev/null +++ b/src/endpoints/admin/custom/mod.rs @@ -0,0 +1,2 @@ +pub mod create_custom; +pub mod update_custom; \ No newline at end of file diff --git a/src/endpoints/admin/custom/update_custom.rs b/src/endpoints/admin/custom/update_custom.rs new file mode 100644 index 00000000..4e40a635 --- /dev/null +++ b/src/endpoints/admin/custom/update_custom.rs @@ -0,0 +1,90 @@ +use crate::models::{QuestTaskDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use crate::utils::verify_task_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateCustom { + id: u32, + name: Option, + desc: Option, + cta: Option, + verify_endpoint: Option, + verify_endpoint_type: Option, + verify_redirect: Option, + href: Option, +}); + +#[route(post, "/admin/tasks/custom/update", crate::endpoints::admin::custom::update_custom)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + + let res= verify_task_auth(user,&collection,&(body.id as i32)).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + // filter to get existing quest + let filter = doc! { + "id": &body.id, + }; + + let mut update_doc = doc! {}; + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(href) = &body.href { + update_doc.insert("href", href); + } + if let Some(cta) = &body.cta { + update_doc.insert("cta", cta); + } + if let Some(verify_redirect) = &body.verify_redirect { + update_doc.insert("verify_redirect", verify_redirect); + } + if let Some(verify_endpoint) = &body.verify_endpoint { + update_doc.insert("verify_endpoint", verify_endpoint); + } + if let Some(verify_endpoint_type) = &body.verify_endpoint_type { + update_doc.insert("verify_endpoint_type", verify_endpoint_type); + } + + // update quest query + let update = doc! { + "$set": update_doc + }; + + + // insert document to boost collection + return match collection + .find_one_and_update(filter, update, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task updated successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error updating tasks".to_string()), + }; +} diff --git a/src/endpoints/admin/delete_task.rs b/src/endpoints/admin/delete_task.rs new file mode 100644 index 00000000..b2069c3f --- /dev/null +++ b/src/endpoints/admin/delete_task.rs @@ -0,0 +1,49 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestTaskDocument,JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_task_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; DeleteTask { + id: i32, +}); + +#[route(post, "/admin/tasks/remove_task", crate::endpoints::admin::delete_task)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + let res= verify_task_auth(user, &collection,&body.id).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + // filter to get existing boost + let filter = doc! { + "id": &body.id, + }; + return match &collection.delete_one(filter.clone(), None).await{ + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "deleted successfully"})), + ) + .into_response(), + Err(_) => { + return get_error("Task does not exist".to_string()); + } + } +} diff --git a/src/endpoints/admin/discord/create_discord.rs b/src/endpoints/admin/discord/create_discord.rs new file mode 100644 index 00000000..56396e5d --- /dev/null +++ b/src/endpoints/admin/discord/create_discord.rs @@ -0,0 +1,84 @@ +use crate::models::{QuestDocument, QuestTaskDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::FindOneOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + + +pub_struct!(Deserialize; CreateCustom { + quest_id: u32, + name: String, + desc: String, + invite_link: String, + guild_id: String, +}); + +#[route(post, "/admin/tasks/discord/create", crate::endpoints::admin::discord::create_discord)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &(body.quest_id as i32)).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + href: body.invite_link.clone(), + quest_id: body.quest_id.clone(), + id: next_id, + cta: "Join now!".to_string(), + verify_endpoint: "quests/discord_fw_callback".to_string(), + verify_endpoint_type: "oauth_discord".to_string(), + task_type: Some("discord".to_string()), + discord_guild_id: Some(body.guild_id.clone()), + quiz_name: None, + verify_redirect: None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, + None, + ) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating task".to_string()), + }; +} diff --git a/src/endpoints/admin/discord/mod.rs b/src/endpoints/admin/discord/mod.rs new file mode 100644 index 00000000..1d7340b7 --- /dev/null +++ b/src/endpoints/admin/discord/mod.rs @@ -0,0 +1,2 @@ +pub mod create_discord; +pub mod update_discord; \ No newline at end of file diff --git a/src/endpoints/admin/discord/update_discord.rs b/src/endpoints/admin/discord/update_discord.rs new file mode 100644 index 00000000..ef6fd5a9 --- /dev/null +++ b/src/endpoints/admin/discord/update_discord.rs @@ -0,0 +1,78 @@ +use crate::models::{QuestTaskDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use crate::utils::verify_task_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateCustom { + id: u32, + name: Option, + desc: Option, + invite_link: Option, + guild_id: Option, +}); + +#[route(post, "/admin/tasks/discord/update", crate::endpoints::admin::discord::update_discord)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + + let res= verify_task_auth(user, &collection,&(body.id as i32)).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + // filter to get existing quest + let filter = doc! { + "id": &body.id, + }; + + let mut update_doc = doc! {}; + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(href) = &body.invite_link { + update_doc.insert("href", href); + } + if let Some(guild_id) = &body.guild_id { + update_doc.insert("discord_guild_id", guild_id); + } + + // update quest query + let update = doc! { + "$set": update_doc + }; + + + // insert document to boost collection + return match collection + .find_one_and_update(filter, update, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task updated successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error updating tasks".to_string()), + }; +} diff --git a/src/endpoints/admin/domain/create_domain.rs b/src/endpoints/admin/domain/create_domain.rs new file mode 100644 index 00000000..7638d536 --- /dev/null +++ b/src/endpoints/admin/domain/create_domain.rs @@ -0,0 +1,79 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuestTaskDocument,JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + + +pub_struct!(Deserialize; CreateTwitterFw { + name: String, + desc: String, + quest_id: i32, +}); + +#[route(post, "/admin/tasks/domain/create", crate::endpoints::admin::domain::create_domain)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &body.quest_id).await; + if !res { + return get_error("Error creating task".to_string()); + }; + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + href: "https://app.starknet.id/".to_string(), + quest_id: body.quest_id.clone() as u32, + id: next_id, + verify_endpoint: "quests/verify_domain".to_string(), + verify_endpoint_type: "default".to_string(), + task_type: Some("domain".to_string()), + cta: "Register a domain".to_string(), + discord_guild_id: None, + quiz_name: None, + verify_redirect: None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating task".to_string()), + }; +} diff --git a/src/endpoints/admin/domain/mod.rs b/src/endpoints/admin/domain/mod.rs new file mode 100644 index 00000000..849ddd1a --- /dev/null +++ b/src/endpoints/admin/domain/mod.rs @@ -0,0 +1,2 @@ +pub mod create_domain; +pub mod update_domain; \ No newline at end of file diff --git a/src/endpoints/admin/domain/update_domain.rs b/src/endpoints/admin/domain/update_domain.rs new file mode 100644 index 00000000..80cbc0a9 --- /dev/null +++ b/src/endpoints/admin/domain/update_domain.rs @@ -0,0 +1,72 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestTaskDocument,JWTClaims}; +use crate::utils::verify_task_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateTwitterFw { + name: Option, + desc: Option, + id: i32, +}); + +#[route(post, "/admin/tasks/domain/update", crate::endpoints::admin::domain::update_domain)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + + + let res= verify_task_auth(user, &collection,&body.id).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + + // filter to get existing quest + let filter = doc! { + "id": &body.id, + }; + + let mut update_doc = doc! {}; + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + + // update quest query + let update = doc! { + "$set": update_doc + }; + + + // insert document to boost collection + return match collection + .find_one_and_update(filter, update, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task updated successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error updating tasks".to_string()), + }; +} diff --git a/src/endpoints/admin/login.rs b/src/endpoints/admin/login.rs new file mode 100644 index 00000000..c2fee9bc --- /dev/null +++ b/src/endpoints/admin/login.rs @@ -0,0 +1,69 @@ +use crate::{ + models::{AppState}, + utils::get_error, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::{doc,from_document}; +use serde::Deserialize; +use std::sync::Arc; +use chrono::Utc; +use serde_json::json; +use crate::models::{JWTClaims, LoginDetails}; +use jsonwebtoken::{encode, Header, EncodingKey}; +use crate::utils::calculate_hash; + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + code: String, +} + +#[route(get, "/admin/login", crate::endpoints::admin::login)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let collection = state.db.collection::("login_details"); + let hashed_code = calculate_hash(&query.code); + let pipeline = [ + doc! { + "$match": { + "code": hashed_code.to_string(), + } + }, + ]; + + + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + let secret_key = &state.conf.auth.secret_key; + if let Ok(login) = from_document::(document) { + let new_exp = (Utc::now().timestamp_millis() + &state.conf.auth.expiry_duration) as usize; + let user_claims = JWTClaims { + sub: login.user.parse().unwrap(), + exp: new_exp, + }; + let token = encode(&Header::default(), &user_claims, &EncodingKey::from_secret(&secret_key.as_ref())).unwrap(); + return (StatusCode::OK, Json(json!({"token":token}))).into_response(); + } + } + Err(e) => { + return get_error(e.to_string()); + } + } + } + get_error("Incorrect Password".to_string()) + } + Err(e) => { + return get_error(e.to_string()); + } + } +} diff --git a/src/endpoints/admin/mod.rs b/src/endpoints/admin/mod.rs new file mode 100644 index 00000000..1d0a1014 --- /dev/null +++ b/src/endpoints/admin/mod.rs @@ -0,0 +1,11 @@ +pub mod login; +pub mod delete_task; +pub mod quiz; +pub mod twitter; +pub mod quest_boost; +pub mod quest; +pub mod custom; +pub mod discord; +pub mod nft_uri; +pub mod domain; +pub mod user; \ No newline at end of file diff --git a/src/endpoints/admin/nft_uri/create_uri.rs b/src/endpoints/admin/nft_uri/create_uri.rs new file mode 100644 index 00000000..c6cf18be --- /dev/null +++ b/src/endpoints/admin/nft_uri/create_uri.rs @@ -0,0 +1,78 @@ +use crate::models::{NFTUri,JWTClaims, QuestDocument}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::FindOneOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; + + +pub_struct!(Deserialize; CreateCustom { + quest_id: u32, + name: String, + desc: String, + image: String, +}); + +#[route(post, "/admin/nft_uri/create", crate::endpoints::admin::nft_uri::create_uri)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("nft_uri"); + + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &(body.quest_id as i32)).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = NFTUri { + name: body.name.clone(), + description: body.desc.clone(), + image: body.image.clone(), + quest_id : body.quest_id.clone() as i64, + id: next_id, + attributes: None, + }; + + // insert document to boost collection + return match collection + .insert_one( + new_document, + None, + ) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Uri created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating boosts".to_string()), + }; +} diff --git a/src/endpoints/admin/nft_uri/get_nft_uri.rs b/src/endpoints/admin/nft_uri/get_nft_uri.rs new file mode 100644 index 00000000..04a0f88a --- /dev/null +++ b/src/endpoints/admin/nft_uri/get_nft_uri.rs @@ -0,0 +1,60 @@ +use crate::models::NFTUri; +use crate::{ + models::{AppState}, + utils::get_error, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use std::sync::Arc; + + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: i64, +} + +#[route( + get, + "/admin/nft_uri/get_nft_uri", + crate::endpoints::admin::nft_uri::get_nft_uri +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let collection = state.db.collection::("nft_uri"); + let pipeline = vec![ + doc! { + "$match": doc! { + "quest_id": query.id + } + }, + doc! { + "$project": doc! { + "_id": 0 + } + }, + ]; + + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + return (StatusCode::OK, Json(document)).into_response(); + } + _ => continue, + } + } + get_error("NFT Uri not found".to_string()) + } + Err(_) => get_error("Error querying quest".to_string()), + } +} diff --git a/src/endpoints/admin/nft_uri/mod.rs b/src/endpoints/admin/nft_uri/mod.rs new file mode 100644 index 00000000..3c6a81f4 --- /dev/null +++ b/src/endpoints/admin/nft_uri/mod.rs @@ -0,0 +1,3 @@ +pub mod create_uri; +pub mod update_uri; +pub mod get_nft_uri; \ No newline at end of file diff --git a/src/endpoints/admin/nft_uri/update_uri.rs b/src/endpoints/admin/nft_uri/update_uri.rs new file mode 100644 index 00000000..69702fbb --- /dev/null +++ b/src/endpoints/admin/nft_uri/update_uri.rs @@ -0,0 +1,68 @@ +use crate::models::{NFTUri,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateCustom { + id: i64, + name: Option, + desc: Option, + image: Option, +}); + +#[route(post, "/admin/nft_uri/update", crate::endpoints::admin::nft_uri::update_uri)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let _user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("nft_uri"); + + // filter to get existing quest + let filter = doc! { + "id": &body.id, + }; + + let mut update_doc = doc! {}; + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("description", desc); + } + if let Some(image) = &body.image { + update_doc.insert("image", image); + } + + // update quest query + let update = doc! { + "$set": update_doc + }; + + + // insert document to boost collection + return match collection + .find_one_and_update(filter, update, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task updated successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error updating tasks".to_string()), + }; +} diff --git a/src/endpoints/admin/quest/create_quest.rs b/src/endpoints/admin/quest/create_quest.rs new file mode 100644 index 00000000..cf384631 --- /dev/null +++ b/src/endpoints/admin/quest/create_quest.rs @@ -0,0 +1,106 @@ +use crate::models::{ QuestInsertDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc, from_document}; +use mongodb::options::FindOneOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + + +pub_struct!(Deserialize; CreateQuestQuery { + name: String, + desc: String, + start_time: i64, + expiry: Option, + disabled: bool, + category: String, + logo: String, + rewards_img: String, + rewards_title: String, + img_card: String, + title_card: String, +}); + +#[route( +post, +"/admin/quest/create", +crate::endpoints::admin::quest::create_quest +)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("quests"); + + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let nft_reward = doc! { + "img": body.rewards_img.clone().to_string(), + "level": 1, + }; + + let mut new_document = doc! { + "name": &body.name, + "desc": &body.desc, + "disabled": &body.disabled, + "start_time": &body.start_time, + "id": &next_id, + "category":&body.category, + "issuer": &user, + "rewards_endpoint":"/quests/claimable", + "rewards_title": &body.rewards_title, + "rewards_img": &body.rewards_img, + "rewards_nfts": vec![nft_reward], + "logo": &body.logo, + "img_card": &body.img_card, + "title_card": &body.title_card, + }; + + match &body.expiry { + Some(expiry) => new_document.insert("expiry", expiry), + None => new_document.insert("expiry", None::), + }; + + match user == "admin" { + true => new_document.insert("experience", 50), + false => new_document.insert("experience", 10), + }; + + // insert document to boost collection + return match collection + .insert_one( + from_document::(new_document).unwrap(), + None, + ) + .await + { + Ok(_res) => { + return ( + StatusCode::OK, + Json(json!({"id": format!("{}",&next_id)})).into_response(), + ) + .into_response(); + } + Err(_e) => get_error("Error creating boosts".to_string()), + }; +} diff --git a/src/endpoints/admin/quest/get_quest.rs b/src/endpoints/admin/quest/get_quest.rs new file mode 100644 index 00000000..1fafe442 --- /dev/null +++ b/src/endpoints/admin/quest/get_quest.rs @@ -0,0 +1,95 @@ +use crate::{ + models::{AppState, QuestDocument,JWTClaims}, + utils::get_error, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use std::sync::Arc; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: i32, +} + +#[route( + get, + "/admin/quest/get_quest", + crate::endpoints::admin::quest::get_quest +)] +pub async fn handler( + State(state): State>, + Query(query): Query, + headers: HeaderMap, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("quests"); + let pipeline = vec![ + doc! { + "$match": doc! { + "id": query.id, + "issuer": user + } + }, + doc! { + "$lookup": doc! { + "from": "boosts", + "let": doc! { + "localFieldValue": "$id" + }, + "pipeline": [ + doc! { + "$match": doc! { + "$expr": doc! { + "$and": [ + doc! { + "$in": [ + "$$localFieldValue", + "$quests" + ] + } + ] + } + } + }, + doc! { + "$project": doc! { + "_id": 0, + "hidden": 0 + } + } + ], + "as": "boosts" + }, + }, + doc! { + "$project": doc! { + "_id": 0 + } + }, + ]; + + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + return (StatusCode::OK, Json(document)).into_response(); + } + _ => continue, + } + } + get_error("Quest not found".to_string()) + } + Err(_) => get_error("Error querying quest".to_string()), + } +} diff --git a/src/endpoints/admin/quest/get_quests.rs b/src/endpoints/admin/quest/get_quests.rs new file mode 100644 index 00000000..aed3e897 --- /dev/null +++ b/src/endpoints/admin/quest/get_quests.rs @@ -0,0 +1,59 @@ +use crate::{ + models::{AppState, JWTClaims, QuestDocument}, + utils::get_error, +}; +use axum::http::HeaderMap; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use mongodb::bson::{doc, from_document}; +use std::sync::Arc; + +#[route( + get, + "/admin/quest/get_quests", + crate::endpoints::admin::quest::get_quests +)] +pub async fn handler(State(state): State>, headers: HeaderMap) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()); + let mut pipeline = vec![]; + if user != "super_user" { + pipeline.push(doc! { + "$match": doc! { + "issuer":user + } + }); + } + let collection = state.db.collection::("quests"); + + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + let mut quests: Vec = Vec::new(); + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + if let Ok(mut quest) = from_document::(document) { + if let Some(expiry) = &quest.expiry { + quest.expiry_timestamp = Some(expiry.to_string()); + } + quests.push(quest); + } + } + _ => continue, + } + } + + if quests.is_empty() { + get_error("No quests found".to_string()) + } else { + (StatusCode::OK, Json(quests)).into_response() + } + } + Err(_) => get_error("Error querying quests".to_string()), + } +} diff --git a/src/endpoints/admin/quest/get_tasks.rs b/src/endpoints/admin/quest/get_tasks.rs new file mode 100644 index 00000000..a77dcb7a --- /dev/null +++ b/src/endpoints/admin/quest/get_tasks.rs @@ -0,0 +1,109 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::stream::StreamExt; +use mongodb::bson::{doc, from_document}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserTask { + id: i64, + quest_id: i64, + name: String, + href: String, + cta: String, + verify_endpoint: String, + verify_endpoint_type: String, + verify_redirect: Option, + desc: String, + quiz_name: Option, + task_type: Option, + discord_guild_id: Option, +} + +#[derive(Deserialize)] +pub struct GetTasksQuery { + quest_id: u32, +} + +#[route(get, "/admin/quest/get_tasks", crate::endpoints::admin::quest::get_tasks)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let pipeline = vec![ + doc! { "$match": { "quest_id": query.quest_id } }, + doc! { + "$lookup": { + "from": "quests", + "localField": "quest_id", + "foreignField": "id", + "as": "quest" + } + }, + doc! { "$unwind": "$quest" }, + doc! { + "$addFields": { + "sort_order": doc! { + "$switch": { + "branches": [ + { + "case": doc! { "$eq": ["$verify_endpoint_type", "quiz"] }, + "then": 1 + }, + { + "case": doc! { "$eq": ["$verify_endpoint_type", "default"] }, + "then": 2 + } + ], + "default": 3 + } + } + } + }, + doc! { "$sort": { "sort_order": 1 } }, + doc! { + "$project": { + "_id": 0, + "id": 1, + "quest_id": 1, + "name": 1, + "href": 1, + "cta": 1, + "verify_endpoint": 1, + "verify_redirect" : 1, + "verify_endpoint_type": 1, + "desc": 1, + "quiz_name": 1, + "task_type":1, + } + }, + ]; + let tasks_collection = state.db.collection::("tasks"); + match tasks_collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + let mut tasks: Vec = Vec::new(); + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + if let Ok(task) = from_document::(document) { + tasks.push(task); + } + } + _ => continue, + } + } + if tasks.is_empty() { + get_error("No tasks found for this quest_id".to_string()) + } else { + (StatusCode::OK, Json(tasks)).into_response() + } + } + Err(_) => get_error("Error querying tasks".to_string()), + } +} diff --git a/src/endpoints/admin/quest/mod.rs b/src/endpoints/admin/quest/mod.rs new file mode 100644 index 00000000..b6d34a69 --- /dev/null +++ b/src/endpoints/admin/quest/mod.rs @@ -0,0 +1,5 @@ +pub mod create_quest; +pub mod update_quest; +pub mod get_quests; +pub mod get_tasks; +mod get_quest; \ No newline at end of file diff --git a/src/endpoints/admin/quest/update_quest.rs b/src/endpoints/admin/quest/update_quest.rs new file mode 100644 index 00000000..a7902c2f --- /dev/null +++ b/src/endpoints/admin/quest/update_quest.rs @@ -0,0 +1,115 @@ +use crate::models::{QuestDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc, Document}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + + +pub_struct!(Deserialize; UpdateQuestQuery { + id: i32, + name: Option, + desc: Option, + start_time: Option, + expiry: Option, + disabled: Option, + category: Option, + logo: Option, + rewards_img: Option, + rewards_title: Option, + img_card: Option, + title_card: Option, +}); + +#[route(post, "/admin/quest/update", crate::endpoints::admin::quest::update_quest)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("quests"); + + // filter to get existing quest + let mut filter = doc! { + "id": &body.id, + }; + + // check if user is super_user + if user != "super_user" { + filter.insert("issuer", user); + } + + let existing_quest = &collection.find_one(filter.clone(), None).await.unwrap(); + if existing_quest.is_none() { + return get_error("quest does not exist".to_string()); + } + + let mut update_doc = Document::new(); + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(expiry) = &body.expiry { + update_doc.insert("expiry", expiry); + } + if let Some(start_time) = &body.start_time { + update_doc.insert("start_time", start_time); + } + if let Some(disabled) = &body.disabled { + update_doc.insert("disabled", disabled); + } + if let Some(category) = &body.category { + update_doc.insert("category", category); + } + if let Some(logo) = &body.logo { + update_doc.insert("logo", logo); + } + if let Some(rewards_img) = &body.rewards_img { + update_doc.insert("rewards_img", rewards_img); + let nft_reward = doc! { + "img": &body.rewards_img.clone(), + "level": 1, + }; + update_doc.insert("rewards_nfts", vec![nft_reward]); + } + if let Some(rewards_title) = &body.rewards_title { + update_doc.insert("rewards_title", rewards_title); + } + if let Some(img_card) = &body.img_card { + update_doc.insert("img_card", img_card); + } + if let Some(title_card) = &body.title_card { + update_doc.insert("title_card", title_card); + } + + + // update quest query + let update = doc! { + "$set": update_doc + }; + + return match collection + .find_one_and_update(filter, update, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating quest".to_string()), + }; +} diff --git a/src/endpoints/admin/quest_boost/create_boost.rs b/src/endpoints/admin/quest_boost/create_boost.rs new file mode 100644 index 00000000..eea84b42 --- /dev/null +++ b/src/endpoints/admin/quest_boost/create_boost.rs @@ -0,0 +1,89 @@ +use crate::models::{BoostTable, QuestDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::FindOneOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +#[derive(Deserialize)] +pub struct CreateBoostQuery { + amount: i32, + token: String, + num_of_winners: i64, + token_decimals: i64, + name: String, + quest_id: i32, + hidden: bool, + expiry: i64, + img_url: String, +} + +#[route( + post, + "/admin/quest_boost/create_boost", + crate::endpoints::admin::quest_boost::create_boost +)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("boosts"); + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &(body.quest_id as i32)).await; + if !res { + return get_error("Error creating boost".to_string()); + }; + + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = BoostTable { + name: body.name.clone(), + amount: body.amount.clone(), + token_decimals: body.token_decimals.clone(), + token:body.token.clone(), + expiry: body.expiry.clone(), + num_of_winners: body.num_of_winners.clone(), + quests: vec![body.quest_id.clone()], + id: next_id, + hidden: body.hidden.clone(), + img_url: body.img_url.clone(), + winner:None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Boost created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating boosts".to_string()), + }; +} diff --git a/src/endpoints/admin/quest_boost/mod.rs b/src/endpoints/admin/quest_boost/mod.rs new file mode 100644 index 00000000..0831f03c --- /dev/null +++ b/src/endpoints/admin/quest_boost/mod.rs @@ -0,0 +1,2 @@ +pub mod update_boost; +pub mod create_boost; \ No newline at end of file diff --git a/src/endpoints/admin/quest_boost/update_boost.rs b/src/endpoints/admin/quest_boost/update_boost.rs new file mode 100644 index 00000000..8bbb8954 --- /dev/null +++ b/src/endpoints/admin/quest_boost/update_boost.rs @@ -0,0 +1,108 @@ +use crate::models::{BoostTable, JWTClaims, QuestDocument}; +use crate::utils::verify_quest_auth; +use crate::{models::AppState, utils::get_error}; +use axum::http::HeaderMap; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use mongodb::bson::{doc, Document}; +use mongodb::options::FindOneAndUpdateOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; + +pub_struct!(Deserialize; UpdateBoostQuery { + id: i32, + amount: Option, + token: Option, + num_of_winners: Option, + token_decimals: Option, + expiry: Option, + name: Option, + img_url: Option, + hidden: Option, +}); + +#[route( +post, +"/admin/quest_boost/update_boost", +crate::endpoints::admin::quest_boost::update_boost +)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("boosts"); + let questcollection = state.db.collection::("quests"); + + let pipeline = doc! { + "id": &body.id, + }; + + let res = &collection.find_one(pipeline, None).await.unwrap(); + if res.is_none() { + return get_error("boost does not exist".to_string()); + } + let quest_id = res.as_ref().unwrap().quests[0]; + let res = verify_quest_auth(user, &questcollection, &(quest_id as i32)).await; + + println!("res: {}", res); + if !res { + return get_error("Error updating boost".to_string()); + }; + + // filter to get existing boost + let filter = doc! { + "id": &body.id, + }; + + let mut update_doc = Document::new(); + + if let Some(amount) = &body.amount { + update_doc.insert("amount", amount); + } + if let Some(token) = &body.token { + update_doc.insert("token", token); + } + if let Some(expiry) = &body.expiry { + update_doc.insert("expiry", expiry); + } + if let Some(num_of_winners) = &body.num_of_winners { + update_doc.insert("num_of_winners", num_of_winners); + } + if let Some(token_decimals) = &body.token_decimals { + update_doc.insert("token_decimals", token_decimals); + } + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(img_url) = &body.img_url { + update_doc.insert("img_url", img_url); + } + if let Some(hidden) = &body.hidden { + update_doc.insert("hidden", hidden); + } + + // update boost + let update = doc! { + "$set": update_doc + }; + let options = FindOneAndUpdateOptions::default(); + return match collection + .find_one_and_update(filter, update, options) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating boost".to_string()), + }; +} diff --git a/src/endpoints/admin/quiz/create_question.rs b/src/endpoints/admin/quiz/create_question.rs new file mode 100644 index 00000000..48430633 --- /dev/null +++ b/src/endpoints/admin/quiz/create_question.rs @@ -0,0 +1,98 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuizInsertDocument, QuizQuestionDocument, JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; +use jsonwebtoken::{Validation, Algorithm, decode, DecodingKey}; + + +pub_struct!(Deserialize; CreateQuizQuestion { + quiz_id: i64, + question: String, + options:Vec, + correct_answers: Vec, +}); + +#[route(post, "/admin/tasks/quiz/question/create", crate::endpoints::admin::quiz::create_question)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let quiz_collection = state.db.collection::("quizzes"); + let quiz_questions_collection = state.db.collection::("quiz_questions"); + let quests_collection = state.db.collection::("quests"); + + let pipeline = doc! { + "$match": { + "quiz_name": &body.quiz_id, + } + }; + let res = &quests_collection.find_one(pipeline, None).await.unwrap(); + if res.is_none() { + return get_error("quiz does not exist".to_string()); + } + + // get the quest id + let quest_id = res.as_ref().unwrap().id as i32; + + + let res = verify_quest_auth(user, &quests_collection, &quest_id).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + // filter to get existing quiz + let filter = doc! { + "id": &body.quiz_id, + }; + + let existing_quiz = &quiz_collection.find_one(filter.clone(), None).await.unwrap(); + if existing_quiz.is_none() { + return get_error("quiz does not exist".to_string()); + } + + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_quiz_question_doc = &quiz_questions_collection.find_one(last_id_filter.clone(), options.clone()).await.unwrap(); + + let mut next_quiz_question_id = 1; + if let Some(doc) = last_quiz_question_doc { + let last_id = doc.id; + next_quiz_question_id = last_id + 1; + } + + let new_quiz_document = QuizQuestionDocument { + quiz_id: body.quiz_id.clone(), + question: body.question.clone(), + options: body.options.clone(), + correct_answers: body.correct_answers.clone(), + id: next_quiz_question_id, + kind: "text_choice".to_string(), + layout: "default".to_string(), + }; + + return match quiz_questions_collection + .insert_one(new_quiz_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => return get_error("Error creating task".to_string()), + }; +} diff --git a/src/endpoints/admin/quiz/create_quiz.rs b/src/endpoints/admin/quiz/create_quiz.rs new file mode 100644 index 00000000..4cceb56e --- /dev/null +++ b/src/endpoints/admin/quiz/create_quiz.rs @@ -0,0 +1,108 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuestTaskDocument, QuizInsertDocument,JWTClaims}; +use crate::utils::verify_quest_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + + +pub_struct!(Deserialize; CreateQuiz { + name: String, + desc: String, + help_link: String, + cta: String, + intro: String, + quest_id: i32, +}); + +#[route(post, "/admin/tasks/quiz/create", crate::endpoints::admin::quiz::create_quiz)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let tasks_collection = state.db.collection::("tasks"); + let quiz_collection = state.db.collection::("quizzes"); + + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &body.quest_id).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_quiz_doc = &quiz_collection.find_one(last_id_filter.clone(), options.clone()).await.unwrap(); + + let mut next_quiz_id = 1; + if let Some(doc) = last_quiz_doc { + let last_id = doc.id; + next_quiz_id = last_id + 1; + } + + let new_quiz_document = QuizInsertDocument { + name: body.name.clone(), + desc: body.desc.clone(), + id: next_quiz_id.clone(), + intro: body.intro.clone(), + }; + + match quiz_collection + .insert_one(new_quiz_document, None) + .await + { + Ok(res) => res, + Err(_e) => return get_error("Error creating quiz".to_string()), + }; + + + let last_task_doc = &tasks_collection.find_one(last_id_filter.clone(), options.clone()).await.unwrap(); + let mut next_id = 1; + if let Some(doc) = last_task_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + href: body.help_link.clone(), + cta: body.cta.clone(), + quest_id: body.quest_id.clone() as u32, + id: next_id.clone(), + verify_endpoint: "/quests/verify_quiz".to_string(), + verify_endpoint_type: "quiz".to_string(), + quiz_name: Some(next_quiz_id.clone() as i64), + task_type: Some("quiz".to_string()), + discord_guild_id: None, + verify_redirect: None, + + }; + + return match tasks_collection + .insert_one(new_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"id": &next_quiz_id })).into_response(), + ) + .into_response(), + Err(_e) => return get_error("Error creating quiz".to_string()), + }; +} diff --git a/src/endpoints/admin/quiz/get_quiz.rs b/src/endpoints/admin/quiz/get_quiz.rs new file mode 100644 index 00000000..c533681e --- /dev/null +++ b/src/endpoints/admin/quiz/get_quiz.rs @@ -0,0 +1,73 @@ +use crate::models::{QuizInsertDocument,JWTClaims}; +use crate::{ + models::{AppState}, + utils::get_error, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use futures::StreamExt; +use mongodb::bson::doc; +use serde::Deserialize; +use std::sync::Arc; +use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; +use axum::http::HeaderMap; + + +#[derive(Deserialize)] +pub struct GetQuestsQuery { + id: i64, +} + +#[route( + get, + "/admin/quiz/get_quiz", + crate::endpoints::admin::quiz::get_quiz, +)] +pub async fn handler( + State(state): State>, + Query(query): Query, + headers: HeaderMap +) -> impl IntoResponse { + let _user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()); + let collection = state.db.collection::("quizzes"); + let pipeline = vec![ + doc! { + "$match": doc! { + "id": query.id + } + }, + doc! { + "$lookup": doc! { + "from": "quiz_questions", + "localField": "id", + "foreignField": "quiz_id", + "as": "questions" + } + }, + doc! { + "$project": doc! { + "_id": 0, + "questions._id": 0 + } + }, + ]; + + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + return (StatusCode::OK, Json(document)).into_response(); + } + _ => continue, + } + } + get_error("NFT Uri not found".to_string()) + } + Err(_) => get_error("Error querying quest".to_string()), + } +} diff --git a/src/endpoints/admin/quiz/mod.rs b/src/endpoints/admin/quiz/mod.rs new file mode 100644 index 00000000..8def435c --- /dev/null +++ b/src/endpoints/admin/quiz/mod.rs @@ -0,0 +1,5 @@ +pub mod create_quiz; +pub mod create_question; +pub mod update_quiz; +pub mod update_question; +pub mod get_quiz; \ No newline at end of file diff --git a/src/endpoints/admin/quiz/update_question.rs b/src/endpoints/admin/quiz/update_question.rs new file mode 100644 index 00000000..5dbf3f58 --- /dev/null +++ b/src/endpoints/admin/quiz/update_question.rs @@ -0,0 +1,96 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc, Document}; +use mongodb::options::{FindOneAndUpdateOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuestTaskDocument, JWTClaims}; +use crate::utils::verify_quest_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation, Algorithm, decode, DecodingKey}; + + +pub_struct!(Deserialize; UpdateQuiz { + id:u32, + question: Option, + options:Option>, + correct_answers: Option, +}); + +#[route(post, "/admin/tasks/quiz/question/update", crate::endpoints::admin::quiz::update_question)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + + let tasks_collection = state.db.collection::("tasks"); + + let quests_collection = state.db.collection::("quests"); + + let pipeline = doc! { + "$match": { + "quiz_name": &body.id, + } + }; + let res = &quests_collection.find_one(pipeline, None).await.unwrap(); + if res.is_none() { + return get_error("quiz does not exist".to_string()); + } + + // get the quest id + let quest_id = res.as_ref().unwrap().id as i32; + + + let res = verify_quest_auth(user, &quests_collection, &quest_id).await; + if res { + return get_error("Error creating task".to_string()); + }; + + // filter to get existing task + let filter = doc! { + "id": &body.id, + }; + let existing_task = &tasks_collection.find_one(filter.clone(), None).await.unwrap(); + + // create a quiz if it does not exist + if existing_task.is_none() { + return get_error("No quiz found".to_string()); + } + + let mut update_doc = Document::new(); + + if let Some(question) = &body.question { + update_doc.insert("question", question); + } + if let Some(options) = &body.options { + update_doc.insert("options", options); + } + if let Some(correct_answers) = &body.correct_answers { + update_doc.insert("correct_answers", correct_answers); + } + + // update question + let update = doc! { + "$set": update_doc, + }; + let options = FindOneAndUpdateOptions::default(); + return match tasks_collection + .find_one_and_update(filter, update, options) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating task".to_string()), + }; +} diff --git a/src/endpoints/admin/quiz/update_quiz.rs b/src/endpoints/admin/quiz/update_quiz.rs new file mode 100644 index 00000000..3b37fff9 --- /dev/null +++ b/src/endpoints/admin/quiz/update_quiz.rs @@ -0,0 +1,87 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneAndUpdateOptions}; +use serde_json::json; +use std::sync::Arc; +use mongodb::bson::Document; +use serde::Deserialize; +use crate::models::{QuestTaskDocument,JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_task_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; UpdateQuiz { + id:u32, + name: Option, + desc: Option, + help_link: Option, + cta: Option, +}); + +#[route(post, "/admin/tasks/quiz/update", crate::endpoints::admin::quiz::update_quiz)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let tasks_collection = state.db.collection::("tasks"); + + + let res= verify_task_auth(user, &tasks_collection,&(body.id as i32)).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + + // filter to get existing boost + let filter = doc! { + "id": &body.id, + }; + let existing_task = &tasks_collection.find_one(filter.clone(), None).await.unwrap(); + + // create a quiz if it does not exist + if existing_task.is_none() { + return get_error("No quiz found".to_string()); + } + + + let mut update_doc = Document::new(); + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(href) = &body.help_link { + update_doc.insert("href", href); + } + if let Some(cta) = &body.cta { + update_doc.insert("cta", cta); + } + + // update boost + let update = doc! { + "$set": update_doc + }; + let options = FindOneAndUpdateOptions::default(); + return match tasks_collection + .find_one_and_update(filter, update, options) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating task".to_string()), + }; +} diff --git a/src/endpoints/admin/twitter/create_twitter_fw.rs b/src/endpoints/admin/twitter/create_twitter_fw.rs new file mode 100644 index 00000000..52f0aead --- /dev/null +++ b/src/endpoints/admin/twitter/create_twitter_fw.rs @@ -0,0 +1,80 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuestTaskDocument,JWTClaims}; +use crate::utils::{verify_quest_auth}; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateTwitterFw { + name: String, + desc: String, + username: String, + quest_id: i32, +}); + +#[route(post, "/admin/tasks/twitter_fw/create", crate::endpoints::admin::twitter::create_twitter_fw)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &body.quest_id).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + verify_redirect: Some(format!("https://twitter.com/intent/user?screen_name={}", body.username.clone())), + href: format!("https://twitter.com/{}", body.username.clone()), + quest_id: body.quest_id.clone() as u32, + id: next_id, + verify_endpoint: "quests/verify_twitter_fw".to_string(), + verify_endpoint_type: "default".to_string(), + task_type: Some("twitter_fw".to_string()), + cta: "Follow".to_string(), + discord_guild_id: None, + quiz_name: None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "Task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating task".to_string()), + }; +} diff --git a/src/endpoints/admin/twitter/create_twitter_rw.rs b/src/endpoints/admin/twitter/create_twitter_rw.rs new file mode 100644 index 00000000..cb60533b --- /dev/null +++ b/src/endpoints/admin/twitter/create_twitter_rw.rs @@ -0,0 +1,80 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use mongodb::options::{FindOneOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestDocument, QuestTaskDocument,JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_quest_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; CreateTwitterRw { + name: String, + desc: String, + post_link: String, + quest_id: i32, +}); + +#[route(post, "/admin/tasks/twitter_rw/create", crate::endpoints::admin::twitter::create_twitter_rw)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + // Get the last id in increasing order + let last_id_filter = doc! {}; + let options = FindOneOptions::builder().sort(doc! {"id": -1}).build(); + let last_doc = &collection.find_one(last_id_filter, options).await.unwrap(); + + let quests_collection = state.db.collection::("quests"); + + + let res= verify_quest_auth(user, &quests_collection, &body.quest_id).await; + if !res { + return get_error("Error creating task".to_string()); + }; + + let mut next_id = 1; + if let Some(doc) = last_doc { + let last_id = doc.id; + next_id = last_id + 1; + } + + let new_document = QuestTaskDocument { + name: body.name.clone(), + desc: body.desc.clone(), + verify_redirect: Some(body.post_link.clone()), + href: body.post_link.clone(), + quest_id: body.quest_id.clone() as u32, + id: next_id, + verify_endpoint: "quests/verify_twitter_rw".to_string(), + verify_endpoint_type: "default".to_string(), + task_type: Some("twitter_rw".to_string()), + cta: "Retweet".to_string(), + discord_guild_id: None, + quiz_name: None, + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, None) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "task created successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating task".to_string()), + }; +} diff --git a/src/endpoints/admin/twitter/mod.rs b/src/endpoints/admin/twitter/mod.rs new file mode 100644 index 00000000..afeabf69 --- /dev/null +++ b/src/endpoints/admin/twitter/mod.rs @@ -0,0 +1,4 @@ +pub mod create_twitter_fw; +pub mod create_twitter_rw; +pub mod update_twitter_fw; +pub mod update_twitter_rw; \ No newline at end of file diff --git a/src/endpoints/admin/twitter/update_twitter_fw.rs b/src/endpoints/admin/twitter/update_twitter_fw.rs new file mode 100644 index 00000000..41db85ac --- /dev/null +++ b/src/endpoints/admin/twitter/update_twitter_fw.rs @@ -0,0 +1,88 @@ +use crate::models::{QuestTaskDocument,JWTClaims}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc, Document}; +use mongodb::options::FindOneAndUpdateOptions; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use crate::utils::verify_task_auth; +use axum::http::HeaderMap; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + +pub_struct!(Deserialize; UpdateTwitterFw { + name: Option, + desc: Option, + username: Option, + id: i32, +}); + +#[route( +put, +"/admin/tasks/twitter_fw/update", +crate::endpoints::admin::twitter::update_twitter_fw +)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + + let collection = state.db.collection::("tasks"); + + let res= verify_task_auth(user, &collection,&body.id).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + // filter to get existing task + let filter = doc! { + "id": &body.id, + }; + let existing_task = &collection.find_one(filter.clone(), None).await.unwrap(); + + // create a task if it does not exist + if existing_task.is_none() { + return get_error("Task does not exist".to_string()); + } + + let mut update_doc = Document::new(); + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(username) = &body.username { + update_doc.insert( + "verify_redirect", + "https://twitter.com/intent/user?screen_name=".to_string() + username, + ); + update_doc.insert("href", "https://twitter.com/".to_string() + username); + } + + // update boost + let update = doc! { + "$set": update_doc + }; + let options = FindOneAndUpdateOptions::default(); + + return match collection + .find_one_and_update(filter, update, options) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating task".to_string()), + }; +} diff --git a/src/endpoints/admin/twitter/update_twitter_rw.rs b/src/endpoints/admin/twitter/update_twitter_rw.rs new file mode 100644 index 00000000..3a07ad05 --- /dev/null +++ b/src/endpoints/admin/twitter/update_twitter_rw.rs @@ -0,0 +1,83 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc, Document}; +use mongodb::options::{FindOneAndUpdateOptions}; +use serde_json::json; +use std::sync::Arc; +use serde::Deserialize; +use crate::models::{QuestTaskDocument,JWTClaims}; +use axum::http::HeaderMap; +use crate::utils::verify_task_auth; +use jsonwebtoken::{Validation,Algorithm,decode,DecodingKey}; + + +pub_struct!(Deserialize; UpdateTwitterRw { + name: Option, + desc: Option, + post_link: Option, + id: i32, +}); + +#[route(put, "/admin/tasks/twitter_rw/update", crate::endpoints::admin::twitter::update_twitter_rw)] +pub async fn handler( + State(state): State>, + headers: HeaderMap, + body: Json, +) -> impl IntoResponse { + let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String; + let collection = state.db.collection::("tasks"); + + + let res= verify_task_auth(user, &collection,&body.id).await; + if !res{ + return get_error("Error updating tasks".to_string()); + } + + + // filter to get existing boost + let filter = doc! { + "id": &body.id, + }; + let existing_task = &collection.find_one(filter.clone(), None).await.unwrap(); + + // create a boost if it does not exist + if existing_task.is_none() { + return get_error("Task does not exist".to_string()); + } + + let mut update_doc = Document::new(); + + if let Some(name) = &body.name { + update_doc.insert("name", name); + } + if let Some(desc) = &body.desc { + update_doc.insert("desc", desc); + } + if let Some(post_link) = &body.post_link { + update_doc.insert("verify_redirect", &post_link); + update_doc.insert("href", &post_link); + } + + // update boost + let update = doc! { + "$set": update_doc + }; + let options = FindOneAndUpdateOptions::default(); + + return match collection + .find_one_and_update(filter, update, options) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "updated successfully"})), + ) + .into_response(), + Err(_e) => get_error("error updating task".to_string()), + }; +} diff --git a/src/endpoints/admin/user/create_user.rs b/src/endpoints/admin/user/create_user.rs new file mode 100644 index 00000000..b88f703e --- /dev/null +++ b/src/endpoints/admin/user/create_user.rs @@ -0,0 +1,48 @@ +use crate::models::{LoginDetails}; +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::State, + http::StatusCode, + response::{IntoResponse, Json}, +}; +use axum_auto_routes::route; +use mongodb::bson::{doc}; +use serde::Deserialize; +use serde_json::json; +use std::sync::Arc; +use crate::utils::calculate_hash; + + +pub_struct!(Deserialize; CreateCustom { + user: String, + password: String, +}); + +#[route(post, "/admin/user/create", crate::endpoints::admin::user::create_user)] +pub async fn handler( + State(state): State>, + body: Json, +) -> impl IntoResponse { + let collection = state.db.collection::("login_details"); + let hashed_password = calculate_hash(&body.password); + + let new_document = LoginDetails { + user: body.user.clone(), + code: hashed_password.to_string(), + }; + + // insert document to boost collection + return match collection + .insert_one(new_document, + None, + ) + .await + { + Ok(_) => ( + StatusCode::OK, + Json(json!({"message": "User added successfully"})).into_response(), + ) + .into_response(), + Err(_e) => get_error("Error creating user".to_string()), + }; +} diff --git a/src/endpoints/admin/user/mod.rs b/src/endpoints/admin/user/mod.rs new file mode 100644 index 00000000..ec60cd9f --- /dev/null +++ b/src/endpoints/admin/user/mod.rs @@ -0,0 +1 @@ +pub mod create_user; \ No newline at end of file diff --git a/src/endpoints/get_quests.rs b/src/endpoints/get_quests.rs index 9000cb25..c424b43c 100644 --- a/src/endpoints/get_quests.rs +++ b/src/endpoints/get_quests.rs @@ -17,8 +17,8 @@ use std::sync::Arc; #[derive(Debug, Serialize, Deserialize)] pub struct NFTItem { - img: String, - level: u32, + pub(crate) img: String, + pub(crate) level: u32, } #[route(get, "/get_quests", crate::endpoints::get_quests)] diff --git a/src/endpoints/get_quiz.rs b/src/endpoints/get_quiz.rs index 272a0d1b..d2083450 100644 --- a/src/endpoints/get_quiz.rs +++ b/src/endpoints/get_quiz.rs @@ -1,70 +1,84 @@ -use crate::{config::QuizQuestionType, models::AppState, utils::get_error}; +use crate::{models::AppState, utils::get_error}; use axum::{ extract::{Query, State}, http::StatusCode, response::{IntoResponse, Json}, }; use axum_auto_routes::route; -use mongodb::bson::doc; -use serde::{Deserialize, Serialize}; +use futures::{StreamExt}; +use mongodb::bson::{doc, Document}; +use serde::{Deserialize}; use starknet::core::types::FieldElement; use std::sync::Arc; #[derive(Deserialize)] pub struct GetQuizQuery { - id: String, + id: i64, // addr could be used as entropy for sending a server side randomized order // let's keep on client side for now #[allow(dead_code)] addr: FieldElement, } -pub_struct!(Clone, Serialize; QuizQuestionResp { - kind: String, - layout: String, - question: String, - options: Vec, - image_for_layout: Option -}); - -#[derive(Clone, Serialize)] -pub struct QuizResponse { - name: String, - desc: String, - questions: Vec, -} #[route(get, "/get_quiz", crate::endpoints::get_quiz)] pub async fn handler( State(state): State>, Query(query): Query, ) -> impl IntoResponse { - let quizzes_from_config = &state.conf.quizzes; - match quizzes_from_config.get(&query.id) { - Some(quiz) => { - let questions: Vec = quiz - .questions - .iter() - .map(|question| QuizQuestionResp { - kind: match question.kind { - QuizQuestionType::TextChoice => "text_choice".to_string(), - QuizQuestionType::ImageChoice => "image_choice".to_string(), - QuizQuestionType::Ordering => "ordering".to_string(), + let collection = state.db.collection::("quizzes"); + let pipeline = vec![ + doc! { + "$match": doc! { + "id": &query.id + } + }, + doc! { + "$lookup": doc! { + "from": "quiz_questions", + "let": doc! { + "id": "$id" + }, + "pipeline": [ + doc! { + "$match": doc! { + "quiz_id": &query.id + } }, - layout: question.layout.clone(), - question: question.question.clone(), - options: question.options.clone(), - image_for_layout: question.image_for_layout.clone(), - }) - .collect(); - let quiz_response = QuizResponse { - name: quiz.name.clone(), - desc: quiz.desc.clone(), - questions, - }; + doc! { + "$project": doc! { + "correct_answers": 0, + "quiz_id": 0, + "_id": 0 + } + } + ], + "as": "questions" + } + }, + doc! { + "$project": doc! { + "_id": 0, + } + }, + ]; - (StatusCode::OK, Json(quiz_response)).into_response() + match collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + match result { + Ok(document) => { + return (StatusCode::OK, Json(document)).into_response(); + } + Err(e) => { + return get_error(e.to_string()); + } + } + } + get_error("Quiz not found".to_string()) + } + Err(e) => { + return get_error(e.to_string()); } - None => get_error("Quiz not found".to_string()), } } diff --git a/src/endpoints/get_tasks.rs b/src/endpoints/get_tasks.rs index 75db0fa7..e75c4325 100644 --- a/src/endpoints/get_tasks.rs +++ b/src/endpoints/get_tasks.rs @@ -23,7 +23,7 @@ pub struct UserTask { verify_redirect: Option, desc: String, completed: bool, - quiz_name: Option, + quiz_name: Option, } #[derive(Deserialize)] diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b180d284..9699ae79 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -14,4 +14,5 @@ pub mod quest_boost; pub mod quests; pub mod get_boosted_quests; pub mod analytics; -pub mod unique_page_visit; \ No newline at end of file +pub mod unique_page_visit; +pub mod admin; \ No newline at end of file diff --git a/src/endpoints/quests/claimable.rs b/src/endpoints/quests/claimable.rs new file mode 100644 index 00000000..a7aff4a5 --- /dev/null +++ b/src/endpoints/quests/claimable.rs @@ -0,0 +1,177 @@ +use crate::{models::AppState, utils::get_error}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, + Json, +}; + +use axum_auto_routes::route; +use futures::TryStreamExt; +use mongodb::bson::{doc, Document}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use starknet::core::types::FieldElement; +use starknet::signers::{LocalWallet, SigningKey}; +use std::sync::Arc; +use crate::models::{Reward, RewardResponse}; +use crate::utils::get_nft; + +#[derive(Debug, Serialize, Deserialize)] +pub struct HasCompletedQuestsQuery { + addr: FieldElement, + quest_id: u32, +} + +#[route(get, "/quests/claimable", crate::endpoints::quests::claimable)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let address = query.addr.to_string(); + let quest_id = query.quest_id; + let pipeline = vec![ + doc! { + "$match": doc! { + "address": address + } + }, + doc! { + "$lookup": doc! { + "from": "tasks", + "localField": "task_id", + "foreignField": "id", + "as": "associatedTask" + } + }, + doc! { + "$unwind": "$associatedTask" + }, + doc! { + "$project": doc! { + "_id": 0, + "address": 1, + "task_id": 1, + "quest_id": "$associatedTask.quest_id" + } + }, + doc! { + "$group": doc! { + "_id": "$quest_id", + "done": doc! { + "$sum": 1 + } + } + }, + doc! { + "$match": doc! { + "_id": quest_id + } + }, + doc! { + "$lookup": doc! { + "from": "tasks", + "localField": "_id", + "foreignField": "quest_id", + "as": "tasks" + } + }, + doc! { + "$project": doc! { + "_id": 1, + "tasks": "$tasks", + "result": doc! { + "$cond": doc! { + "if": doc! { + "$eq": [ + doc! { + "$size": "$tasks" + }, + "$done" + ] + }, + "then": true, + "else": false + } + } + } + }, + doc! { + "$unwind": doc! { + "path": "$tasks" + } + }, + doc! { + "$sort": doc! { + "tasks.id": -1 + } + }, + doc! { + "$limit": 1 + }, + doc! { + "$lookup": doc! { + "from": "nft_uri", + "localField": "_id", + "foreignField": "quest_id", + "as": "nft_uri" + } + }, + doc! { + "$project": doc! { + "result": 1, + "_id": 0, + "last_task": "$tasks.id", + "nft_level": "$nft_uri.id" + } + }, + doc! { + "$unwind": doc! { + "path": "$nft_level" + } + }, + ]; + let tasks_collection = state.db.collection::("completed_tasks"); + match tasks_collection.aggregate(pipeline, None).await { + Ok(cursor) => { + let mut cursor = cursor; + let mut result = false; + let mut nft_level = 0; + let mut last_task = 0; + while let Some(doc) = cursor.try_next().await.unwrap() { + result = doc.get("result").unwrap().as_bool().unwrap(); + nft_level = doc.get("nft_level").unwrap().as_i64().unwrap(); + last_task = doc.get("last_task").unwrap().as_i32().unwrap(); + } + + if !result { + return get_error("User hasn't completed all tasks".to_string()); + } + + let signer = LocalWallet::from(SigningKey::from_secret_scalar( + state.conf.nft_contract.private_key, + )); + + let mut rewards = vec![]; + + let Ok((token_id, sig)) = + get_nft(quest_id, last_task as u32, &query.addr, nft_level as u32, &signer).await + else { + return get_error("Signature failed".into()); + }; + + rewards.push(Reward { + task_id: last_task as u32, + nft_contract: state.conf.nft_contract.address.clone(), + token_id: token_id.to_string(), + sig: (sig.r, sig.s), + }); + + if rewards.is_empty() { + get_error("No rewards found for this user".into()) + } else { + (StatusCode::OK, Json(RewardResponse { rewards })).into_response() + } + } + Err(_) => get_error("Error querying status".to_string()), + } +} diff --git a/src/endpoints/quests/discord_fw_callback.rs b/src/endpoints/quests/discord_fw_callback.rs new file mode 100644 index 00000000..3341d092 --- /dev/null +++ b/src/endpoints/quests/discord_fw_callback.rs @@ -0,0 +1,157 @@ +use std::sync::Arc; + +use crate::utils::CompletedTasksTrait; +use crate::{ + models::AppState, + utils::{get_error_redirect, success_redirect}, +}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, +}; +use axum_auto_routes::route; +use mongodb::bson::doc; +use reqwest::header::AUTHORIZATION; +use serde::Deserialize; +use starknet::core::types::FieldElement; +use crate::models::QuestTaskDocument; + +#[derive(Deserialize)] +pub struct TwitterOAuthCallbackQuery { + code: String, + state: String, +} + +#[derive(Deserialize, Debug)] +pub struct Guild { + id: String, + #[allow(dead_code)] + name: String, +} + +#[route(get, "/quests/discord_fw_callback", crate::endpoints::quests::discord_fw_callback)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + // the state is in format => "address+quest_id+task_id" + let state_split = query.state.split('+').collect::>(); + let quest_id = state_split[1].parse::().unwrap(); + let task_id = state_split[2].parse::().unwrap(); + let addr = FieldElement::from_dec_str(state_split[0]).unwrap(); + + let tasks_collection = state.db.collection::("tasks"); + let task = tasks_collection + .find_one(doc! { "id": task_id,"quest_id":quest_id,"task_type":"discord" }, None) + .await + .unwrap() + .unwrap(); + + let guild_id = task.discord_guild_id.unwrap(); + + let authorization_code = &query.code; + let error_redirect_uri = format!( + "{}/quest/{}?task_id={}&res=false", + state.conf.variables.app_link, quest_id, task_id + ); + + // Exchange the authorization code for an access token + let params = [ + ("client_id", &state.conf.discord.oauth2_clientid), + ("client_secret", &state.conf.discord.oauth2_secret), + ("code", &authorization_code.to_string()), + ( + "redirect_uri", + &format!( + "{}/quests/discord_fw_callback", + state.conf.variables.api_link + ), + ), + ("grant_type", &"authorization_code".to_string()), + ]; + let access_token = match exchange_authorization_code(params).await { + Ok(token) => token, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to exchange authorization code: {}", e), + ); + } + }; + + // Get user guild information + let client = reqwest::Client::new(); + let response_result = client + .get("https://discord.com/api/users/@me/guilds") + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .send() + .await; + let response: Vec = match response_result { + Ok(response) => { + let json_result = response.json().await; + match json_result { + Ok(json) => json, + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!( + "Failed to get JSON response while fetching user info: {}", + e + ), + ); + } + } + } + Err(e) => { + return get_error_redirect( + error_redirect_uri, + format!("Failed to send request to get user info: {}", e), + ); + } + }; + + for guild in response { + if guild.id == guild_id { + print!("Checking guild: {:?}", guild); + match state.upsert_completed_task(addr, task_id).await { + Ok(_) => { + let redirect_uri = format!( + "{}/quest/{}?task_id={}&res=true", + state.conf.variables.app_link, quest_id, task_id + ); + return success_redirect(redirect_uri); + } + Err(e) => return get_error_redirect(error_redirect_uri, format!("{}", e)), + } + } + } + + get_error_redirect( + error_redirect_uri, + "You're not part of the Discord server".to_string(), + ) +} + +async fn exchange_authorization_code( + params: [(&str, &String); 5], +) -> Result> { + let client = reqwest::Client::new(); + let res = client + .post("https://discord.com/api/oauth2/token") + .form(¶ms) + .send() + .await?; + let json: serde_json::Value = res.json().await?; + match json["access_token"].as_str() { + Some(s) => Ok(s.to_string()), + None => { + Err(Box::new(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Failed to get 'access_token' from JSON response : {:?}", + json + ), + ))) + } + } +} diff --git a/src/endpoints/quests/ekubo/mod.rs b/src/endpoints/quests/ekubo/mod.rs index 45150758..2e450cc6 100644 --- a/src/endpoints/quests/ekubo/mod.rs +++ b/src/endpoints/quests/ekubo/mod.rs @@ -1,4 +1,3 @@ pub mod claimable; pub mod discord_fw_callback; -pub mod verify_added_liquidity; -pub mod verify_quiz; +pub mod verify_added_liquidity; \ No newline at end of file diff --git a/src/endpoints/quests/ekubo/verify_quiz.rs b/src/endpoints/quests/ekubo/verify_quiz.rs deleted file mode 100644 index 9cf7fb47..00000000 --- a/src/endpoints/quests/ekubo/verify_quiz.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; - -use crate::{ - common::verify_quiz::verify_quiz, - models::{AppState, VerifyQuizQuery}, - utils::{get_error, CompletedTasksTrait}, -}; -use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; -use axum_auto_routes::route; -use serde_json::json; -use starknet::core::types::FieldElement; - -#[route( - post, - "/quests/ekubo/verify_quiz", - crate::endpoints::quests::ekubo::verify_quiz -)] -pub async fn handler( - State(state): State>, - body: Json, -) -> impl IntoResponse { - let task_id = 37; - if body.addr == FieldElement::ZERO { - return get_error("Please connect your wallet first".to_string()); - } - - let user_answers_numbers: Result>, _> = body - .user_answers_list - .iter() - .map(|inner_list| { - inner_list - .iter() - .map(|s| s.parse::()) - .collect::, _>>() - }) - .collect(); - - match user_answers_numbers { - Ok(responses) => match verify_quiz(&state.conf, body.addr, &body.quiz_name, &responses) { - true => match state.upsert_completed_task(body.addr, task_id).await { - Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), - Err(e) => get_error(format!("{}", e)), - }, - false => get_error("Incorrect answers".to_string()), - }, - Err(e) => get_error(format!("{}", e)), - } -} diff --git a/src/endpoints/quests/mod.rs b/src/endpoints/quests/mod.rs index 9f39461d..8fd2db62 100644 --- a/src/endpoints/quests/mod.rs +++ b/src/endpoints/quests/mod.rs @@ -22,4 +22,8 @@ pub mod rango; pub mod nimbora; pub mod hashstack; pub mod haiko; +pub mod claimable; +pub mod discord_fw_callback; +pub mod verify_twitter_rw; +pub mod verify_twitter_fw; pub mod bountive; diff --git a/src/endpoints/quests/rango/quest1/check_trade.rs b/src/endpoints/quests/rango/quest1/check_trade.rs index afc25bcb..1e6a7ca6 100644 --- a/src/endpoints/quests/rango/quest1/check_trade.rs +++ b/src/endpoints/quests/rango/quest1/check_trade.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::models::VerifyQuery; -use crate::utils::{to_hex, CompletedTasksTrait}; +use crate::utils::{to_hex, CompletedTasksTrait, make_api_request}; use crate::{models::AppState, utils::get_error}; use axum::{ extract::{Query, State}, @@ -13,9 +13,9 @@ use axum_auto_routes::route; use serde_json::json; #[route( - get, - "/quests/rango/check_trade", - crate::endpoints::quests::rango::quest1::check_trade +get, +"/quests/rango/check_trade", +crate::endpoints::quests::rango::quest1::check_trade )] pub async fn handler( State(state): State>, @@ -37,49 +37,22 @@ pub async fn handler( address_hex.insert(0, 'x'); address_hex.insert(0, '0'); - let res = make_rango_request( + let res = make_api_request( &state.conf.rango.api_endpoint, - &state.conf.rango.api_key, &address_hex, + Some(&state.conf.rango.api_key), ) - .await; - let response = match res { - Ok(response) => response, - Err(_) => return get_error(format!("Try again later")), - }; - if let Some(_) = response.get("data") { - let data = response.get("data").unwrap(); - if let Some(res) = data.get("result") { - if res.as_bool().unwrap() { - return match state.upsert_completed_task(query.addr, task_id).await { - Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), - Err(e) => get_error(format!("{}", e)), - }; - } - } - } - get_error("User has not completed the task".to_string()) -} + .await; -async fn make_rango_request( - endpoint: &str, - api_key: &str, - addr: &str, -) -> Result { - let client = reqwest::Client::new(); - match client - .post(endpoint) - .json(&json!({ - "address": addr, - })) - .header("apiKey", api_key) - .send() - .await - { - Ok(response) => match response.json::().await { - Ok(json) => Ok(json), - Err(_) => Err(format!("Funds not bridged")), - }, - Err(_) => Err(format!("Funds not bridged")), + match res { + true => { + return match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + }; + } + false => { + get_error("User has not completed the task".to_string()) + } } -} +} \ No newline at end of file diff --git a/src/endpoints/quests/uri.rs b/src/endpoints/quests/uri.rs index 561a4f30..26577aa1 100644 --- a/src/endpoints/quests/uri.rs +++ b/src/endpoints/quests/uri.rs @@ -1,4 +1,4 @@ -use crate::models::AppState; +use crate::models::{AppState, NFTUri}; use crate::utils::get_error; use axum::{ extract::{Query, State}, @@ -9,6 +9,8 @@ use axum::{ use axum_auto_routes::route; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use futures::StreamExt; +use mongodb::bson::{doc, from_document}; #[derive(Serialize)] pub struct TokenURI { @@ -18,7 +20,7 @@ pub struct TokenURI { attributes: Option>, } -#[derive(Serialize)] +#[derive(Serialize,Deserialize)] pub struct Attribute { trait_type: String, value: u32, @@ -36,435 +38,38 @@ pub async fn handler( ) -> Response { let level = level_query .level - .and_then(|level_str| level_str.parse::().ok()); - - fn get_level(level_int: u32) -> &'static str { - match level_int { - 12 => "Chef", - 11 => "Officer", - 10 => "Soldier", - 2 => "Silver", - 3 => "Gold", - _ => "Bronze", + .and_then(|level_str| level_str.parse::().ok()); + + let uri_collection = state.db.collection::("nft_uri"); + let pipeline = vec![ + doc! { + "$match":{ + "id":&level.unwrap() + } } - } - - match level { - Some(level_int) if level_int > 0 && level_int <= 3 => { - let image_link = format!( - "{}/starkfighter/level{}.webp", - state.conf.variables.app_link, level_int - ); - let response = TokenURI { - name: format!("StarkFighter {} Arcade", get_level(level_int)), - description: "A starknet.quest NFT won during the Starkfighter event.".into(), - image: image_link, - attributes: Some(vec![Attribute { - trait_type: "level".into(), - value: level_int, - }]), - }; - (StatusCode::OK, Json(response)).into_response() + ]; + + match uri_collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(result) = cursor.next().await { + return match result { + Ok(document) => { + if let Ok(nft_uri) = from_document::(document) { + return (StatusCode::OK, + Json(TokenURI { + name: (&*nft_uri.name).to_string(), + description: (&*nft_uri.description).to_string(), + image: format!("{}{}", state.conf.variables.app_link, &*nft_uri.image), + attributes: None, + })).into_response() + } + get_error("Error querying NFT URI".to_string()) + } + _ => get_error("Error querying NFT URI".to_string()), + }; + } + get_error("NFT URI not found".to_string()) } - - Some(4) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet ID Tribe Totem".into(), - description: "A Starknet Quest NFT won for creating a StarknetID profile.".into(), - image: format!("{}/starknetid/nft1.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(5) => ( - StatusCode::OK, - Json(TokenURI { - name: "JediSwap Light Saber".into(), - description: "A JediSwap NFT won for interacting with the protocol.".into(), - image: format!("{}/jediswap/padawan.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(6) => ( - StatusCode::OK, - Json(TokenURI { - name: "AVNU Astronaut".into(), - description: "An AVNU NFT won for interacting with the protocol.".into(), - image: format!("{}/avnu/astronaut.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(7) => ( - StatusCode::OK, - Json(TokenURI { - name: "Sithswap Helmet".into(), - description: "A Sithswap NFT won for interacting with the protocol.".into(), - image: format!( - "{}/sithswap/sith_helmet.webp", - state.conf.variables.app_link - ), - attributes: None, - }), - ) - .into_response(), - - Some(8) => ( - StatusCode::OK, - Json(TokenURI { - name: "Zklend Artemis".into(), - description: "A Zklend NFT won for interacting with the protocol.".into(), - image: format!("{}/zklend/artemis.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(9) => ( - StatusCode::OK, - Json(TokenURI { - name: "Stark Tribe Shield".into(), - description: "A Starknet Quest NFT won for showing allegiance to the Stark Tribe." - .into(), - image: format!("{}/starknetid/shield.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(level_int) if level_int > 9 && level_int <= 12 => { - let image_link = format!( - "{}/starknetid/necklace{}.webp", - state.conf.variables.app_link, - level_int - 9 - ); - let response = TokenURI { - name: format!("Starknet ID {} Necklace", get_level(level_int)), - description: "A Starknet Quest NFT won during a Starknet ID quest.".into(), - image: image_link, - attributes: Some(vec![Attribute { - trait_type: "level".into(), - value: level_int, - }]), - }; - (StatusCode::OK, Json(response)).into_response() - } - - Some(13) => ( - StatusCode::OK, - Json(TokenURI { - name: "StarkOrb".into(), - description: "An Orbiter NFT won for interacting with the protocol.".into(), - image: format!("{}/orbiter/orbiter.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(14) => ( - StatusCode::OK, - Json(TokenURI { - name: "Ekubo".into(), - description: "An Ekubo NFT won for interacting with the protocol.".into(), - image: format!("{}/ekubo/concentration.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(15) => ( - StatusCode::OK, - Json(TokenURI { - name: "Carmine".into(), - description: "A Carmine NFT won for interacting with the protocol.".into(), - image: format!("{}/carmine/specialist.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(16) => ( - StatusCode::OK, - Json(TokenURI { - name: "Morphine".into(), - description: "A Morphine NFT won for interacting with the protocol.".into(), - image: format!("{}/morphine/yielder.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(17) => ( - StatusCode::OK, - Json(TokenURI { - name: "MySwap".into(), - description: "A MySwap NFT won for interacting with the protocol.".into(), - image: format!("{}/myswap/LP.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(18) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x Starknet ID Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first step into the Starknet universe. By getting a Stark domain name and becoming a Whisperer of Braavos, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/starknetid.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(19) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Giga Brain NFT".into(), - description: "A Starknet Giga Brain NFT won for successfuly responding to a quiz.".into(), - image: format!("{}/starknet/gigabrain.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(20) => ( - StatusCode::OK, - Json(TokenURI { - name: "Account Abstraction Mastery NFT".into(), - description: "An Account Abstraction Mastery NFT won for successfully responding to a quiz.".into(), - image: format!("{}/starknet/aa.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(21) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Focus Tree".into(), - description: "The Focus Tree NFT won during a Starknet Quest.".into(), - image: format!("{}/focustree/focustree.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(22) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Element Gemstone".into(), - description: "An Element Gemstone NFT won for successfully finishing the Quest".into(), - image: format!("{}/element/elementGem.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(23) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Briq Element Gemstone".into(), - description: "A Briq Element Gemstone NFT won for successfully finishing the Quest".into(), - image: format!("{}/element/briqGem.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(24) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Layerswap Element Gemstone".into(), - description: "A Layerswap Element Gemstone NFT won for successfully finishing the Quest".into(), - image: format!("{}/element/layerswapGem.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - - Some(25) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Starknet.id Element Gemstone".into(), - description: "A Starknet.id Element Gemstone NFT won for successfully finishing the Quest".into(), - image: format!("{}/element/starknetidGem.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(26) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x mySwap Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By using mySwap and becoming a Whisperer of Braavos, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/myswap.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(27) => ( - StatusCode::OK, - Json(TokenURI { - name: "Nostra - LaFamiglia Rose".into(), - description: "A Nostra - LaFamiglia Rose NFT won for successfully finishing the Quest".into(), - image: format!("{}/nostra/rose.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(28) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x AVNU Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By using AVNU you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/avnu.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(29) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x Braavos Wallet Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By using Braavos Wallet and becoming a Whisperer of Braavos, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/wallet.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(30) => ( - StatusCode::OK, - Json(TokenURI { - name: "Rango Exchange Castle Bridge NFT".into(), - description: "A Rango Exchange Quest NFT won for successfully finishing the Quest".into(), - image: format!("{}/rango/bridge.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(31) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Silver Rhino NFT".into(), - description: "Completing the Quest successfully makes you eligible to win a Silver Rhino NFT".into(), - image: format!("{}/rhino/silverRhino.webp", state.conf.variables.app_link), - attributes: None, - }), - ) - .into_response(), - - Some(32) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x Pyramid Market Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By using Pyramid NFT Marketplace, a new NFT Marketplace, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/pyramid.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(33) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x zkLend Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By using zkLend, the native money-market protocol on Starknet, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/zklend.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(36) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x Realms: Loot Survivor Quest NFT".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By playing Loot Survivor, the first Loot adventure game exploring the Play2Die mechanic on Starknet, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/realms.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(35) => ( - StatusCode::OK, - Json(TokenURI { - name: "The Nimbora Pool".into(), - description: "A Nimbora NFT won for successfully finishing the Quest. Nimbora is bridging Ethereum's Layer 1 and Layer 2 seamlessly for cost-efficient DeFi interactions with improved user experience and uncompromised pooling.".into(), - image: format!("{}/nimbora/pool.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(37) => ( - StatusCode::OK, - Json(TokenURI { - name: "Golden Castle Bridge".into(), - description: "A Rango Exchange Golden Castle Bridge NFT won for successfully finishing the Quest".into(), - image: format!("{}/rango/golden_castle_bridge.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(38) => ( - StatusCode::OK, - Json(TokenURI { - name: "Starknet Pro Score x Carbonable Quest NFT ".into(), - description: "This Starknet commemorative Non-Fungible Token represents the first steps into the Starknet universe. By interacting with Carbonable, the first real world asset platform on Starknet, you are building solid foundations for your Starknet experience.".into(), - image: format!("{}/braavos/carbonable.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(39) => ( - StatusCode::OK, - Json(TokenURI { - name: "Nostra - Mafia Boss Cigar NFT".into(), - description: "A Nostra - Mafia Boss Cigar NFT won for successfully finishing the Quest".into(), - image: format!("{}/nostra/cigar.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(40) => ( - StatusCode::OK, - Json(TokenURI { - name: "Hashstack Winquest NFT ".into(), - description: "A Hashstack Winquest NFT won for successfully finishing the Quest".into(), - image: format!("{}/hashstack/hashstackEmpire.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(41) => ( - StatusCode::OK, - Json(TokenURI { - name: "Haiko Strategist NFT ".into(), - description: "A Haiko Strategist NFT won for successfully finishing the Quest".into(), - image: format!("{}/haiko/haikoStrategist.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - Some(42) => ( - StatusCode::OK, - Json(TokenURI { - name: "Bountive Jackpot Journey NFT ".into(), - description: "A Bountive Jackpot Journey won for successfully finishing the Quest".into(), - image: format!("{}/bountive/bountiveJackpot.webp", state.conf.variables.app_link), - attributes: None, - }), - ).into_response(), - - - _ => get_error("Error, this level is not correct".into()), + Err(_) => get_error("Error querying NFT URI".to_string()), } } diff --git a/src/endpoints/quests/verify_domain.rs b/src/endpoints/quests/verify_domain.rs new file mode 100644 index 00000000..bb703e94 --- /dev/null +++ b/src/endpoints/quests/verify_domain.rs @@ -0,0 +1,22 @@ +use crate::{ + common::verify_has_root_or_braavos_domain::verify_has_root_or_braavos_domain, + models::{AppState, VerifyQuery}, +}; +use axum::{ + extract::{Query, State}, + response::IntoResponse, +}; +use axum_auto_routes::route; +use std::sync::Arc; + +#[route( +get, +"/quests/verify_has_domain", +crate::endpoints::quests::verify_domain +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + verify_has_root_or_braavos_domain(state, &query.addr, 82).await +} diff --git a/src/endpoints/quests/verify_quiz.rs b/src/endpoints/quests/verify_quiz.rs index b9580daa..21b65668 100644 --- a/src/endpoints/quests/verify_quiz.rs +++ b/src/endpoints/quests/verify_quiz.rs @@ -1,4 +1,4 @@ - use std::sync::Arc; +use std::sync::Arc; use crate::{ common::verify_quiz::verify_quiz, @@ -7,38 +7,11 @@ use crate::{ }; use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; use axum_auto_routes::route; +use futures::TryStreamExt; +use mongodb::bson::doc; use serde_json::json; use starknet::core::types::FieldElement; - -fn get_task_id(quiz_name: &str) -> Option { - match quiz_name { - "carmine" => Some(40), - "morphine" => Some(42), - "zklend" => Some(53), - "avnu" => Some(54), - "sithswap" => Some(55), - "starknetid" => Some(56), - "gigabrain_1" => Some(51), - "gigabrain_2" => Some(57), - "gigabrain_3" => Some(58), - "aa_mastery_1" => Some(52), - "aa_mastery_2" => Some(59), - "aa_mastery_3" => Some(60), - "focustree" => Some(61), - "element" => Some(64), - "briq" => Some(67), - "element_starknetid" => Some(73), - "nostra" => Some(79), - "rango" => Some(95), - "braavos" => Some(98), - "rhino" => Some(100), - "nimbora" => Some(89), - "nostra2" => Some(132), - "haiko" => Some(140), - "bountive" => Some(145), - _ => None, - } -} +use crate::models::QuestTaskDocument; #[route(post, "/quests/verify_quiz", crate::endpoints::quests::verify_quiz)] pub async fn handler( @@ -49,9 +22,24 @@ pub async fn handler( return get_error("Please connect your wallet first".to_string()); } - let task_id = match get_task_id(&body.quiz_name) { - Some(id) => id, - None => return get_error("Quiz name does not match".to_string()), + let pipeline = vec![ + doc! { + "$match": doc! { + "quiz_name": &body.quiz_name + } + }, + ]; + + let tasks_collection = state.db.collection::("tasks"); + let task_id = match tasks_collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + let mut id = 0; + while let Some(result) = cursor.try_next().await.unwrap() { + id = result.get("id").unwrap().as_i64().unwrap(); + } + id as u32 + } + Err(_) => return get_error("Quiz name does not match".to_string()), }; let user_answers_numbers: Result>, _> = body @@ -66,7 +54,7 @@ pub async fn handler( .collect(); match user_answers_numbers { - Ok(responses) => match verify_quiz(&state.conf, body.addr, &body.quiz_name, &responses) { + Ok(responses) => match verify_quiz(&state.db, body.addr, &body.quiz_name, &responses).await { true => match state.upsert_completed_task(body.addr, task_id).await { Ok(_) => (StatusCode::OK, Json(json!({"res": true}))).into_response(), Err(e) => get_error(format!("{}", e)), diff --git a/src/endpoints/quests/verify_twitter_fw.rs b/src/endpoints/quests/verify_twitter_fw.rs new file mode 100644 index 00000000..67912ff3 --- /dev/null +++ b/src/endpoints/quests/verify_twitter_fw.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use futures::{ TryStreamExt}; +use mongodb::bson::doc; +use serde_json::json; +use crate::models::QuestTaskDocument; + +#[route( +get, +"/quests/verify_twitter_fw", +crate::endpoints::quests::verify_twitter_fw +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = query.quest_id; + let task_id = query.task_id; + let pipeline = vec![ + doc! { + "$match": doc! { + "quest_id": quest_id, + "id":task_id , + "task_type": "twitter_fw" + } + }, + ]; + + let tasks_collection = state.db.collection::("tasks"); + match tasks_collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(_result) = cursor.try_next().await.unwrap() { + match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => return (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + }; + } + get_error("Error querying task".to_string()) + } + Err(e) => get_error(e.to_string()), + } +} diff --git a/src/endpoints/quests/verify_twitter_rw.rs b/src/endpoints/quests/verify_twitter_rw.rs new file mode 100644 index 00000000..01ec268e --- /dev/null +++ b/src/endpoints/quests/verify_twitter_rw.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use crate::models::QuestTaskDocument; +use crate::{ + models::{AppState, VerifyQuery}, + utils::{get_error, CompletedTasksTrait}, +}; +use axum::{ + extract::{Query, State}, + http::StatusCode, + response::IntoResponse, + Json, +}; +use axum_auto_routes::route; +use futures::TryStreamExt; +use mongodb::bson::doc; +use serde_json::json; + +#[route( + get, + "/quests/verify_twitter_rw", + crate::endpoints::quests::verify_twitter_rw +)] +pub async fn handler( + State(state): State>, + Query(query): Query, +) -> impl IntoResponse { + let quest_id = query.quest_id; + let task_id = query.task_id; + let pipeline = vec![doc! { + "$match": doc! { + "quest_id": quest_id, + "id":task_id, + "task_type": "twitter_rw" + } + }]; + + let tasks_collection = state.db.collection::("tasks"); + match tasks_collection.aggregate(pipeline, None).await { + Ok(mut cursor) => { + while let Some(_result) = cursor.try_next().await.unwrap() { + match state.upsert_completed_task(query.addr, task_id).await { + Ok(_) => return (StatusCode::OK, Json(json!({"res": true}))).into_response(), + Err(e) => get_error(format!("{}", e)), + }; + } + get_error("Error querying task".to_string()) + } + Err(e) => get_error(e.to_string()), + } +} diff --git a/src/main.rs b/src/main.rs index 9aac0b64..7bf38ef9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use starknet::providers::{jsonrpc::HttpTransport, JsonRpcClient}; use std::sync::Arc; use std::{net::SocketAddr, sync::Mutex}; use utils::WithState; - use crate::utils::{add_leaderboard_table, run_boosts_raffle}; use tower_http::cors::{Any, CorsLayer}; diff --git a/src/models.rs b/src/models.rs index db7832f8..ba2103ee 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use mongodb::{Database}; +use mongodb::Database; use serde::{Deserialize, Serialize}; use serde_json::Value; use starknet::{ @@ -7,6 +7,7 @@ use starknet::{ }; use crate::config::Config; +use crate::endpoints::quests::uri::Attribute; pub_struct!(;AppState { conf: Config, @@ -36,13 +37,60 @@ pub_struct!(Debug, Serialize, Deserialize; QuestDocument { title_card: String, hidden: Option, disabled: bool, - expiry: Option, + expiry: Option, expiry_timestamp: Option, mandatory_domain: Option, expired: Option, experience: i64, - start_time: i64, + start_time: i64, +}); + +pub_struct!(Debug, Serialize, Deserialize; QuestInsertDocument { + id: u32, + name: String, + desc: String, + additional_desc: Option, + issuer: String, + category: String, + rewards_endpoint: String, + logo: String, + rewards_img: String, + rewards_title: String, + rewards_description: Option, + rewards_nfts: Vec, + img_card: String, + title_card: String, + disabled: bool, + expiry: Option, + mandatory_domain: Option, + experience: i64, + start_time: i64, +}); + +pub_struct!(Debug, Serialize, Deserialize; QuizInsertDocument { + id: u32, + name: String, + desc: String, + intro:String, +}); +pub_struct!(Debug, Serialize, Deserialize; QuizQuestionDocument { + id: i64, + question: String, + options:Vec, + correct_answers: Vec, + kind: String, + layout: String, + quiz_id: i64, +}); + +pub_struct!(Serialize, Deserialize; NFTUri { + id: i64, + name: String, + description:String, + image: String, + quest_id: i64, + attributes: Option }); pub_struct!(Deserialize; CompletedTasks { @@ -58,19 +106,24 @@ pub struct CompletedTaskDocument { timestamp: i64, } - -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Serialize, Deserialize, Default)] pub struct QuestTaskDocument { - id: u32, - quest_id: u32, - name: String, - desc: String, - cta: String, - verify_endpoint: String, - verify_endpoint_type: String, - verify_redirect: Option, - href: String, - quiz_name: Option, + pub(crate) id: i32, + pub quest_id: u32, + pub name: String, + pub desc: String, + pub cta: String, + pub verify_endpoint: String, + pub href: String, + pub verify_endpoint_type: String, + #[serde(default)] + pub verify_redirect: Option, + #[serde(default)] + pub quiz_name: Option, + #[serde(default)] + pub task_type: Option, + #[serde(default)] + pub(crate) discord_guild_id: Option, } pub_struct!(Serialize; Reward { @@ -85,7 +138,9 @@ pub_struct!(Serialize; RewardResponse { }); pub_struct!(Deserialize; VerifyQuery { - addr: FieldElement, + addr: FieldElement, + quest_id: u32, + task_id: u32, }); pub_struct!(Deserialize; EmailQuery { @@ -95,7 +150,7 @@ pub_struct!(Deserialize; EmailQuery { pub_struct!(Deserialize; VerifyQuizQuery { addr: FieldElement, - quiz_name: String, + quiz_name: i64, user_answers_list: Vec>, }); @@ -264,3 +319,24 @@ pub_struct!(Debug, Serialize, Deserialize; QuestCategoryDocument { desc: String, img_url: String, }); + +pub_struct!(Debug, Serialize, Deserialize; JWTClaims { + sub: String, + exp: usize, +}); + +pub_struct!(Debug, Serialize, Deserialize; LoginDetails { + user: String, + code: String, +}); + +pub_struct!(Deserialize; CreateBoostQuery { + quest_id: i32, + amount: i32, + token: String, + num_of_winners: i64, + token_decimals: i64, + name: String, + img_url: String, + expiry: i64, +}); diff --git a/src/utils.rs b/src/utils.rs index 54f6bf69..50e7f773 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,4 @@ -use crate::models::{ - AchievementDocument, AppState, BoostTable, CompletedTasks, LeaderboardTable, UserExperience, -}; +use crate::models::{AchievementDocument, AppState, BoostTable, CompletedTasks, LeaderboardTable, QuestDocument, QuestTaskDocument, UserExperience}; use async_trait::async_trait; use axum::{ body::Body, @@ -15,6 +13,7 @@ use mongodb::{ IndexModel, }; use rand::distributions::{Distribution, Uniform}; +use serde_json::json; use starknet::signers::Signer; use starknet::{ core::{ @@ -23,6 +22,8 @@ use starknet::{ }, signers::LocalWallet, }; +use std::collections::hash_map::DefaultHasher; +use std::hash::{Hash, Hasher}; use std::result::Result; use std::str::FromStr; use std::{fmt::Write, sync::Arc}; @@ -38,6 +39,35 @@ macro_rules! pub_struct { } } +macro_rules! check_authorization { + ($headers:expr,$secret_key:expr) => { + match $headers.get("Authorization") { + Some(auth_header) => { + let validation = Validation::new(Algorithm::HS256); + let token = auth_header + .to_str() + .unwrap() + .to_string() + .split(" ") + .collect::>()[1] + .to_string(); + + match decode::( + &token, + &DecodingKey::from_secret($secret_key), + &validation, + ) { + Ok(token_data) => token_data.claims.sub, + Err(_e) => { + return get_error("Invalid token".to_string()); + } + } + } + None => return get_error("missing auth header".to_string()), + } + }; +} + pub async fn get_nft( quest_id: u32, task_id: u32, @@ -63,6 +93,12 @@ pub async fn get_nft( Ok((token_id, sig)) } +pub fn calculate_hash(t: &String) -> u64 { + let mut hasher = DefaultHasher::new(); + t.hash(&mut hasher); + hasher.finish() +} + pub fn get_error(error: String) -> Response { (StatusCode::INTERNAL_SERVER_ERROR, error).into_response() } @@ -242,7 +278,7 @@ impl CompletedTasksTrait for AppState { experience.into(), timestamp, ) - .await; + .await; } Err(_e) => { get_error("Error querying quests".to_string()); @@ -251,7 +287,6 @@ impl CompletedTasksTrait for AppState { } None => {} } - Ok(result) } } @@ -296,8 +331,7 @@ impl AchievementsTrait for AppState { let achieved_collection: Collection = self.db.collection("achieved"); let created_at = Utc::now().timestamp_millis(); let filter = doc! { "addr": addr.to_string(), "achievement_id": achievement_id }; - let update = - doc! { "$setOnInsert": { "addr": addr.to_string(), "achievement_id": achievement_id , "timestamp":created_at } }; + let update = doc! { "$setOnInsert": { "addr": addr.to_string(), "achievement_id": achievement_id , "timestamp":created_at } }; let options = UpdateOptions::builder().upsert(true).build(); let result = achieved_collection @@ -332,7 +366,7 @@ impl AchievementsTrait for AppState { experience.into(), timestamp, ) - .await; + .await; } None => {} } @@ -696,6 +730,104 @@ pub fn run_boosts_raffle(db: &Database, interval: u64) { )); } +pub async fn verify_task_auth( + user: String, + task_collection: &Collection, + id: &i32, +) -> bool { + if user == "super_user" { + return true; + } + + let pipeline = vec![ + doc! { + "$match": doc! { + "id": id + } + }, + doc! { + "$lookup": doc! { + "from": "quests", + "localField": "quest_id", + "foreignField": "id", + "as": "quest" + } + }, + doc! { + "$project": doc! { + "quest.issuer": 1 + } + }, + doc! { + "$unwind": doc! { + "path": "$quest" + } + }, + doc! { + "$project": doc! { + "issuer": "$quest.issuer" + } + }, + ]; + let mut existing_quest = task_collection.aggregate(pipeline, None).await.unwrap(); + + let mut issuer = String::new(); + while let Some(doc) = existing_quest.try_next().await.unwrap() { + issuer = doc.get("issuer").unwrap().as_str().unwrap().to_string(); + } + if issuer == user { + return true; + } + false +} + +pub async fn verify_quest_auth( + user: String, + quest_collection: &Collection, + id: &i32, +) -> bool { + if user == "super_user" { + return true; + } + + let filter = doc! { "id": id, "issuer": user }; + + let existing_quest = quest_collection.find_one(filter, None).await.unwrap(); + + match existing_quest { + Some(_) => true, + None => false, + } + +} +pub async fn make_api_request(endpoint: &str, addr: &str, api_key: Option<&str>) -> bool { + let client = reqwest::Client::new(); + let request_builder = client.post(endpoint).json(&json!({ + "address": addr, + })); + let key = api_key.unwrap_or(""); + let request_builder = match key.is_empty() { + true => request_builder, + false => request_builder.header("apiKey", key), + }; + match request_builder.send().await { + Ok(response) => match response.json::().await { + Ok(json) => { + //check value of result in json + if let Some(data) = json.get("data") { + if let Some(res) = data.get("result") { + return res.as_bool().unwrap(); + } + } + false + } + Err(_) => false, + }, + Err(_) => false, + }; + false +} + // required for axum_auto_routes pub trait WithState: Send { fn to_router(self: Box, shared_state: Arc) -> Router;