Skip to content

Commit 4f28c50

Browse files
authored
feat: allow creators to check the state of each user playing their quest (#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
1 parent d060f80 commit 4f28c50

36 files changed

+689
-399
lines changed

crates/db/src/core/definitions.rs

+10-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub trait QuestsDatabase: Send + Sync + CloneDatabase {
3030
async fn is_active_quest(&self, quest_id: &str) -> DBResult<bool>;
3131
async fn has_active_quest_instance(&self, user_address: &str, quest_id: &str)
3232
-> DBResult<bool>;
33+
async fn is_quest_creator(&self, quest_id: &str, creator_address: &str) -> DBResult<bool>;
3334

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

51-
async fn get_quest_instances_by_quest_id(
52+
async fn get_all_quest_instances_by_quest_id(
5253
&self,
5354
quest_id: &str,
5455
) -> DBResult<(Vec<QuestInstance>, Vec<QuestInstance>)>;
56+
async fn get_active_quest_instances_by_quest_id(
57+
&self,
58+
quest_id: &str,
59+
offset: i64,
60+
limit: i64,
61+
) -> DBResult<Vec<QuestInstance>>;
5562

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

88-
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
95+
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
8996
pub struct Event {
9097
pub id: String,
9198
pub user_address: String,
@@ -94,7 +101,7 @@ pub struct Event {
94101
pub event: Vec<u8>,
95102
}
96103

97-
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
104+
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug, ToSchema)]
98105
pub struct QuestInstance {
99106
pub id: String,
100107
pub quest_id: String,

crates/db/src/core/tests.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ pub async fn quest_database_works<DB: QuestsDatabase>(db: &DB, quest: CreateQues
99
assert!(db.ping().await);
1010
let quest_id = db.create_quest(&quest, "0xA").await.unwrap();
1111

12+
let is_creator = db.is_quest_creator(&quest_id, "0xA").await.unwrap();
13+
assert!(is_creator);
14+
15+
let is_not_creator = db.is_quest_creator(&quest_id, "0xB").await.unwrap();
16+
assert!(!is_not_creator);
17+
1218
let quest_reward = db.get_quest_reward_hook(&quest_id).await.unwrap_err();
1319

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

135-
let instances_by_quest_id = db.get_quest_instances_by_quest_id(&quest_id).await.unwrap();
141+
let quest_instances = db
142+
.get_active_quest_instances_by_quest_id(&quest_id, 0, 50)
143+
.await
144+
.unwrap();
145+
146+
assert_eq!(quest_instances.len(), 1);
147+
148+
let instances_by_quest_id = db
149+
.get_all_quest_instances_by_quest_id(&quest_id)
150+
.await
151+
.unwrap();
136152

137153
assert_eq!(instances_by_quest_id.0.len(), 1);
138154
assert_eq!(instances_by_quest_id.1.len(), 0);

crates/db/src/lib.rs

+44-1
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ impl QuestsDatabase for Database {
560560
self.do_get_quest_reward_items(quest_id, None).await
561561
}
562562

563-
async fn get_quest_instances_by_quest_id(
563+
async fn get_all_quest_instances_by_quest_id(
564564
&self,
565565
quest_id: &str,
566566
) -> DBResult<(Vec<QuestInstance>, Vec<QuestInstance>)> {
@@ -601,6 +601,49 @@ impl QuestsDatabase for Database {
601601
Ok((actives, not_actives))
602602
}
603603

604+
async fn get_active_quest_instances_by_quest_id(
605+
&self,
606+
quest_id: &str,
607+
offset: i64,
608+
limit: i64,
609+
) -> DBResult<Vec<QuestInstance>> {
610+
let instances = sqlx::query(
611+
"SELECT * FROM quest_instances
612+
WHERE quest_id = $1
613+
AND id NOT IN (SELECT quest_instance_id as id FROM abandoned_quest_instances)
614+
OFFSET $2 LIMIT $3",
615+
)
616+
.bind(parse_str_to_uuid(quest_id)?)
617+
.bind(offset)
618+
.bind(limit)
619+
.fetch_all(&self.pool)
620+
.await
621+
.map_err(|err| {
622+
DBError::GetQuestInstancesByQuestIdFailed(quest_id.to_string(), Box::new(err))
623+
})?;
624+
625+
let result: Result<Vec<_>, _> =
626+
instances.into_iter().map(QuestInstance::try_from).collect();
627+
628+
result.map_err(|err| DBError::RowCorrupted(Box::new(err)))
629+
}
630+
631+
async fn is_quest_creator(&self, quest_id: &str, creator_address: &str) -> DBResult<bool> {
632+
let quest_exists: bool = sqlx::query_scalar(
633+
"
634+
SELECT EXISTS (SELECT 1 FROM quests
635+
WHERE id = $1 AND creator_address = $2)
636+
",
637+
)
638+
.bind(parse_str_to_uuid(quest_id)?)
639+
.bind(creator_address)
640+
.fetch_one(&self.pool)
641+
.await
642+
.map_err(|err| DBError::CanActivateQuestFailed(Box::new(err)))?;
643+
644+
Ok(quest_exists)
645+
}
646+
604647
async fn can_activate_quest(&self, quest_id: &str) -> DBResult<bool> {
605648
let quest_exists: bool = sqlx::query_scalar(
606649
"

crates/protocol/src/quests/state.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ impl From<&QuestGraph> for QuestState {
104104
}
105105
}
106106

107-
pub fn get_state(quest: &Quest, events: Vec<Event>) -> QuestState {
107+
pub fn get_state(quest: &Quest, events: &[Event]) -> QuestState {
108108
let quest_graph = QuestGraph::from(quest);
109109
let initial_state = (&quest_graph).into();
110110
events.iter().fold(initial_state, |state, event| {

crates/server/src/api/middlewares/auth.rs

-76
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use actix_web::http::header::HeaderMap;
2+
use dcl_crypto_middleware_rs::signed_fetch::{verify, AuthMiddlewareError, VerificationOptions};
3+
use std::collections::HashMap;
4+
5+
pub mod optional_auth;
6+
pub mod required_auth;
7+
8+
async fn verification(
9+
headers: &HeaderMap,
10+
method: &str,
11+
path: &str,
12+
) -> Result<String, AuthMiddlewareError> {
13+
let headers = headers
14+
.iter()
15+
.map(|(key, val)| (key.to_string(), val.to_str().unwrap_or("").to_string()))
16+
.collect::<HashMap<String, String>>();
17+
18+
verify(method, path, headers, VerificationOptions::default())
19+
.await
20+
.map(|address| address.to_string().to_ascii_lowercase())
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
2+
use serde::Deserialize;
3+
use std::{future::Future, pin::Pin};
4+
5+
#[derive(Deserialize, Debug, Default, Clone)]
6+
pub struct OptionalAuthUser {
7+
pub address: Option<String>,
8+
}
9+
10+
impl FromRequest for OptionalAuthUser {
11+
type Error = Error;
12+
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
13+
14+
fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
15+
let request = request.clone();
16+
Box::pin(async move {
17+
match super::verification(request.headers(), request.method().as_str(), request.path())
18+
.await
19+
{
20+
Ok(address) => Ok(OptionalAuthUser {
21+
address: Some(address),
22+
}),
23+
// since it's optional auth, we do not return unathorization error
24+
Err(_) => Ok(OptionalAuthUser { address: None }),
25+
}
26+
})
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use actix_web::{dev::Payload, error::ErrorUnauthorized, Error, FromRequest, HttpRequest};
2+
use serde::Deserialize;
3+
use std::{future::Future, pin::Pin};
4+
5+
#[derive(Deserialize, Debug, Default, Clone)]
6+
pub struct RequiredAuthUser {
7+
pub address: String,
8+
}
9+
10+
impl FromRequest for RequiredAuthUser {
11+
type Error = Error;
12+
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
13+
14+
fn from_request(request: &HttpRequest, _: &mut Payload) -> Self::Future {
15+
let request = request.clone();
16+
Box::pin(async move {
17+
super::verification(request.headers(), request.method().as_str(), request.path())
18+
.await
19+
.map(|address| RequiredAuthUser { address })
20+
.map_err(|_| ErrorUnauthorized("Unathorized"))
21+
})
22+
}
23+
}

crates/server/src/api/middlewares/mod.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ mod metrics_token;
33
mod tracing;
44

55
pub use self::tracing::initialize_telemetry;
6-
pub use auth::dcl_auth_middleware;
6+
pub use auth::optional_auth::OptionalAuthUser;
7+
pub use auth::required_auth::RequiredAuthUser;
78
pub use metrics_token::metrics_token;

crates/server/src/api/mod.rs

-15
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,6 @@ pub fn get_app_router(
6060
.app_data(db.clone())
6161
.app_data(redis.clone())
6262
.app_data(metrics_collector.clone())
63-
.wrap(middlewares::dcl_auth_middleware(
64-
[
65-
"POST:/api/quests",
66-
"DELETE:/api/quests/{quest_id}",
67-
"PUT:/api/quests/{quest_id}",
68-
"GET:/api/quests/{quest_id}/stats",
69-
"PUT:/api/quests/{quest_id}/activate",
70-
"PATCH:/api/instances/{quest_instance}/reset",
71-
],
72-
[
73-
"GET:/api/quests/{quest_id}",
74-
"GET:/api/quests/{quest_id}/reward",
75-
"GET:/api/creators/{user_address}/quests",
76-
],
77-
))
7863
.wrap(dcl_http_prom_metrics::metrics())
7964
.wrap(middlewares::metrics_token(&config.wkc_metrics_bearer_token))
8065
.wrap(TracingLogger::default())

crates/server/src/api/routes/api_doc.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::creators;
22
use super::health;
3+
use super::quest_instances;
34
use super::quests;
45
use actix_web::web::ServiceConfig;
56
use actix_web_lab::__reexports::serde_json::{json, to_value};
@@ -26,7 +27,10 @@ use utoipa_redoc::Servable;
2627
quests::get_quest_stats,
2728
quests::activate_quest,
2829
quests::get_quest_updates,
30+
quests::get_quest_instances,
2931
creators::get_quests_by_creator_id,
32+
quest_instances::reset_quest_instance,
33+
quest_instances::get_quest_instance_state
3034
),
3135
components(
3236
schemas(
@@ -47,14 +51,23 @@ use utoipa_redoc::Servable;
4751
quests_protocol::definitions::Task,
4852
quests_protocol::definitions::Action,
4953
quests_protocol::definitions::Connection,
54+
quests_protocol::definitions::QuestState,
55+
quests_protocol::definitions::StepContent,
56+
quests_protocol::definitions::Task,
5057
quests_db::core::definitions::QuestReward,
5158
quests_db::core::definitions::QuestRewardHook,
5259
quests_db::core::definitions::QuestRewardItem,
60+
quests_db::core::definitions::Event,
61+
quests_db::core::definitions::QuestInstance,
62+
quest_instances::state::GetInstanceStateResponse,
63+
quests::get_instances::GetQuestInstancesResponse,
64+
quests::get_instances::GetQuestInstancesQuery
5365
)
5466
),
5567
tags(
5668
(name = "quests", description = "Quests endpoints."),
57-
(name = "creators", description = "Creators endpoints.")
69+
(name = "creators", description = "Creators endpoints."),
70+
(name = "quest_instances", description = "Quest Instances endpoints.")
5871
),
5972
)]
6073
struct ApiDoc;

0 commit comments

Comments
 (0)