Skip to content

Commit

Permalink
feat: allow creators to check the state of each user playing their qu…
Browse files Browse the repository at this point in the history
…est (#176)

This PR: 
- adds a new endpoint to allow the creators to get all instances of
their quests
- adds a new endpoint to allow the creators to check the state of each
instance
- improves auth middleware and error conversion
  • Loading branch information
lauti7 authored Jun 11, 2024
1 parent d060f80 commit 4f28c50
Show file tree
Hide file tree
Showing 36 changed files with 689 additions and 399 deletions.
13 changes: 10 additions & 3 deletions crates/db/src/core/definitions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub trait QuestsDatabase: Send + Sync + CloneDatabase {
async fn is_active_quest(&self, quest_id: &str) -> DBResult<bool>;
async fn has_active_quest_instance(&self, user_address: &str, quest_id: &str)
-> DBResult<bool>;
async fn is_quest_creator(&self, quest_id: &str, creator_address: &str) -> DBResult<bool>;

async fn start_quest(&self, quest_id: &str, user_address: &str) -> DBResult<String>;
async fn abandon_quest_instance(&self, quest_instance_id: &str) -> DBResult<String>;
Expand All @@ -48,10 +49,16 @@ pub trait QuestsDatabase: Send + Sync + CloneDatabase {
user_address: &str,
) -> DBResult<Vec<QuestInstance>>;

async fn get_quest_instances_by_quest_id(
async fn get_all_quest_instances_by_quest_id(
&self,
quest_id: &str,
) -> DBResult<(Vec<QuestInstance>, Vec<QuestInstance>)>;
async fn get_active_quest_instances_by_quest_id(
&self,
quest_id: &str,
offset: i64,
limit: i64,
) -> DBResult<Vec<QuestInstance>>;

async fn add_event(&self, event: &AddEvent, quest_instance_id: &str) -> DBResult<()>;
async fn get_events(&self, quest_instance_id: &str) -> DBResult<Vec<Event>>;
Expand Down Expand Up @@ -85,7 +92,7 @@ pub struct AddEvent<'a> {
pub event: Vec<u8>,
}

#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
pub struct Event {
pub id: String,
pub user_address: String,
Expand All @@ -94,7 +101,7 @@ pub struct Event {
pub event: Vec<u8>,
}

#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
pub struct QuestInstance {
pub id: String,
pub quest_id: String,
Expand Down
18 changes: 17 additions & 1 deletion crates/db/src/core/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
assert!(db.ping().await);
let quest_id = db.create_quest(&quest, "0xA").await.unwrap();

let is_creator = db.is_quest_creator(&quest_id, "0xA").await.unwrap();
assert!(is_creator);

let is_not_creator = db.is_quest_creator(&quest_id, "0xB").await.unwrap();
assert!(!is_not_creator);

let quest_reward = db.get_quest_reward_hook(&quest_id).await.unwrap_err();

assert!(matches!(quest_reward, DBError::RowNotFound));
Expand Down Expand Up @@ -132,7 +138,17 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
assert_eq!(get_quest_instance.user_address, "0xA");
assert_eq!(get_quest_instance.quest_id, quest_id);

let instances_by_quest_id = db.get_quest_instances_by_quest_id(&quest_id).await.unwrap();
let quest_instances = db
.get_active_quest_instances_by_quest_id(&quest_id, 0, 50)
.await
.unwrap();

assert_eq!(quest_instances.len(), 1);

let instances_by_quest_id = db
.get_all_quest_instances_by_quest_id(&quest_id)
.await
.unwrap();

assert_eq!(instances_by_quest_id.0.len(), 1);
assert_eq!(instances_by_quest_id.1.len(), 0);
Expand Down
45 changes: 44 additions & 1 deletion crates/db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ impl QuestsDatabase for Database {
self.do_get_quest_reward_items(quest_id, None).await
}

async fn get_quest_instances_by_quest_id(
async fn get_all_quest_instances_by_quest_id(
&self,
quest_id: &str,
) -> DBResult<(Vec<QuestInstance>, Vec<QuestInstance>)> {
Expand Down Expand Up @@ -601,6 +601,49 @@ impl QuestsDatabase for Database {
Ok((actives, not_actives))
}

async fn get_active_quest_instances_by_quest_id(
&self,
quest_id: &str,
offset: i64,
limit: i64,
) -> DBResult<Vec<QuestInstance>> {
let instances = sqlx::query(
"SELECT * FROM quest_instances
WHERE quest_id = $1
AND id NOT IN (SELECT quest_instance_id as id FROM abandoned_quest_instances)
OFFSET $2 LIMIT $3",
)
.bind(parse_str_to_uuid(quest_id)?)
.bind(offset)
.bind(limit)
.fetch_all(&self.pool)
.await
.map_err(|err| {
DBError::GetQuestInstancesByQuestIdFailed(quest_id.to_string(), Box::new(err))
})?;

let result: Result<Vec<_>, _> =
instances.into_iter().map(QuestInstance::try_from).collect();

result.map_err(|err| DBError::RowCorrupted(Box::new(err)))
}

async fn is_quest_creator(&self, quest_id: &str, creator_address: &str) -> DBResult<bool> {
let quest_exists: bool = sqlx::query_scalar(
"
SELECT EXISTS (SELECT 1 FROM quests
WHERE id = $1 AND creator_address = $2)
",
)
.bind(parse_str_to_uuid(quest_id)?)
.bind(creator_address)
.fetch_one(&self.pool)
.await
.map_err(|err| DBError::CanActivateQuestFailed(Box::new(err)))?;

Ok(quest_exists)
}

async fn can_activate_quest(&self, quest_id: &str) -> DBResult<bool> {
let quest_exists: bool = sqlx::query_scalar(
"
Expand Down
2 changes: 1 addition & 1 deletion crates/protocol/src/quests/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl From<&QuestGraph> for QuestState {
}
}

pub fn get_state(quest: &Quest, events: Vec<Event>) -> QuestState {
pub fn get_state(quest: &Quest, events: &[Event]) -> QuestState {
let quest_graph = QuestGraph::from(quest);
let initial_state = (&quest_graph).into();
events.iter().fold(initial_state, |state, event| {
Expand Down
76 changes: 0 additions & 76 deletions crates/server/src/api/middlewares/auth.rs

This file was deleted.

21 changes: 21 additions & 0 deletions crates/server/src/api/middlewares/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use actix_web::http::header::HeaderMap;
use dcl_crypto_middleware_rs::signed_fetch::{verify, AuthMiddlewareError, VerificationOptions};
use std::collections::HashMap;

pub mod optional_auth;
pub mod required_auth;

async fn verification(
headers: &HeaderMap,
method: &str,
path: &str,
) -> Result<String, AuthMiddlewareError> {
let headers = headers
.iter()
.map(|(key, val)| (key.to_string(), val.to_str().unwrap_or("").to_string()))
.collect::<HashMap<String, String>>();

verify(method, path, headers, VerificationOptions::default())
.await
.map(|address| address.to_string().to_ascii_lowercase())
}
28 changes: 28 additions & 0 deletions crates/server/src/api/middlewares/auth/optional_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use serde::Deserialize;
use std::{future::Future, pin::Pin};

#[derive(Deserialize, Debug, Default, Clone)]
pub struct OptionalAuthUser {
pub address: Option<String>,
}

impl FromRequest for OptionalAuthUser {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
let request = request.clone();
Box::pin(async move {
match super::verification(request.headers(), request.method().as_str(), request.path())
.await
{
Ok(address) => Ok(OptionalAuthUser {
address: Some(address),
}),
// since it's optional auth, we do not return unathorization error
Err(_) => Ok(OptionalAuthUser { address: None }),
}
})
}
}
23 changes: 23 additions & 0 deletions crates/server/src/api/middlewares/auth/required_auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use actix_web::{dev::Payload, error::ErrorUnauthorized, Error, FromRequest, HttpRequest};
use serde::Deserialize;
use std::{future::Future, pin::Pin};

#[derive(Deserialize, Debug, Default, Clone)]
pub struct RequiredAuthUser {
pub address: String,
}

impl FromRequest for RequiredAuthUser {
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
let request = request.clone();
Box::pin(async move {
super::verification(request.headers(), request.method().as_str(), request.path())
.await
.map(|address| RequiredAuthUser { address })
.map_err(|_| ErrorUnauthorized("Unathorized"))
})
}
}
3 changes: 2 additions & 1 deletion crates/server/src/api/middlewares/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ mod metrics_token;
mod tracing;

pub use self::tracing::initialize_telemetry;
pub use auth::dcl_auth_middleware;
pub use auth::optional_auth::OptionalAuthUser;
pub use auth::required_auth::RequiredAuthUser;
pub use metrics_token::metrics_token;
15 changes: 0 additions & 15 deletions crates/server/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,21 +60,6 @@ pub fn get_app_router(
.app_data(db.clone())
.app_data(redis.clone())
.app_data(metrics_collector.clone())
.wrap(middlewares::dcl_auth_middleware(
[
"POST:/api/quests",
"DELETE:/api/quests/{quest_id}",
"PUT:/api/quests/{quest_id}",
"GET:/api/quests/{quest_id}/stats",
"PUT:/api/quests/{quest_id}/activate",
"PATCH:/api/instances/{quest_instance}/reset",
],
[
"GET:/api/quests/{quest_id}",
"GET:/api/quests/{quest_id}/reward",
"GET:/api/creators/{user_address}/quests",
],
))
.wrap(dcl_http_prom_metrics::metrics())
.wrap(middlewares::metrics_token(&config.wkc_metrics_bearer_token))
.wrap(TracingLogger::default())
Expand Down
15 changes: 14 additions & 1 deletion crates/server/src/api/routes/api_doc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use super::creators;
use super::health;
use super::quest_instances;
use super::quests;
use actix_web::web::ServiceConfig;
use actix_web_lab::__reexports::serde_json::{json, to_value};
Expand All @@ -26,7 +27,10 @@ use utoipa_redoc::Servable;
quests::get_quest_stats,
quests::activate_quest,
quests::get_quest_updates,
quests::get_quest_instances,
creators::get_quests_by_creator_id,
quest_instances::reset_quest_instance,
quest_instances::get_quest_instance_state
),
components(
schemas(
Expand All @@ -47,14 +51,23 @@ use utoipa_redoc::Servable;
quests_protocol::definitions::Task,
quests_protocol::definitions::Action,
quests_protocol::definitions::Connection,
quests_protocol::definitions::QuestState,
quests_protocol::definitions::StepContent,
quests_protocol::definitions::Task,
quests_db::core::definitions::QuestReward,
quests_db::core::definitions::QuestRewardHook,
quests_db::core::definitions::QuestRewardItem,
quests_db::core::definitions::Event,
quests_db::core::definitions::QuestInstance,
quest_instances::state::GetInstanceStateResponse,
quests::get_instances::GetQuestInstancesResponse,
quests::get_instances::GetQuestInstancesQuery
)
),
tags(
(name = "quests", description = "Quests endpoints."),
(name = "creators", description = "Creators endpoints.")
(name = "creators", description = "Creators endpoints."),
(name = "quest_instances", description = "Quest Instances endpoints.")
),
)]
struct ApiDoc;
Expand Down
Loading

0 comments on commit 4f28c50

Please sign in to comment.