Skip to content

Commit

Permalink
Merge pull request #208 from starknet-id/ayush/admin-dashboard-endpoints
Browse files Browse the repository at this point in the history
feat: admin dashboard endpoints
  • Loading branch information
Th0rgal authored Jun 7, 2024
2 parents ead556f + ee9fa7b commit 832b631
Show file tree
Hide file tree
Showing 61 changed files with 3,309 additions and 666 deletions.
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
axum-client-ip = "0.4.0"
jsonwebtoken = "9"
tower = "0.4.13"
4 changes: 4 additions & 0 deletions config.template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ port = 8080
name = "starkship_server"
connection_string = "xxxxxx"

[auth]
secret_key = "secret_key"
expiry_duration = 0

[rhino]
api_endpoint="XXXXXXXXXXXX"

Expand Down
2 changes: 1 addition & 1 deletion src/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
117 changes: 88 additions & 29 deletions src/common/verify_quiz.rs
Original file line number Diff line number Diff line change
@@ -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<usize>, vector2: &Vec<usize>) -> 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<Vec<usize>>,
) -> 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::<QuizInsertDocument>("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;
}
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
correct_answers: Option<Vec<usize>>,
correct_order: Option<Vec<usize>>,
correct_order: Option<Vec<String>>,
image_for_layout: Option<String>,
});

pub_struct!(Clone, Deserialize; Quiz {
pub_struct!(Clone, Deserialize,Debug; Quiz {
name: String,
desc: String,
questions: Vec<QuizQuestion>,
Expand Down Expand Up @@ -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,
Expand All @@ -183,6 +188,7 @@ pub_struct!(Clone, Deserialize; Config {
rhino: PublicApi,
rango: Api,
pyramid: ApiEndpoint,
auth:AuthSetup,
});

pub fn load() -> Config {
Expand Down
83 changes: 83 additions & 0 deletions src/endpoints/admin/custom/create_custom.rs
Original file line number Diff line number Diff line change
@@ -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<Arc<AppState>>,
headers: HeaderMap,
body: Json<CreateCustom>,
) -> impl IntoResponse {
let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String;
let collection = state.db.collection::<QuestTaskDocument>("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::<QuestDocument>("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()),
};
}
2 changes: 2 additions & 0 deletions src/endpoints/admin/custom/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod create_custom;
pub mod update_custom;
90 changes: 90 additions & 0 deletions src/endpoints/admin/custom/update_custom.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
desc: Option<String>,
cta: Option<String>,
verify_endpoint: Option<String>,
verify_endpoint_type: Option<String>,
verify_redirect: Option<String>,
href: Option<String>,
});

#[route(post, "/admin/tasks/custom/update", crate::endpoints::admin::custom::update_custom)]
pub async fn handler(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
body: Json<CreateCustom>,
) -> impl IntoResponse {
let user = check_authorization!(headers, &state.conf.auth.secret_key.as_ref()) as String;
let collection = state.db.collection::<QuestTaskDocument>("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()),
};
}
Loading

0 comments on commit 832b631

Please sign in to comment.