survey/src/api/v1/mcaptcha/db.rs

510 lines
14 KiB
Rust

/*
* Copyright (C) 2023 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use url::Url;
use crate::api::v1::get_random;
use crate::errors::*;
use crate::mcaptcha::PerformanceAnalytics;
use crate::Data;
impl Data {
/// Check if an mCaptcha instance is registered on the database
pub async fn mcaptcha_url_exists(&self, url: &str) -> ServiceResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from survey_mcaptcha_hostname WHERE url = $1)",
url
)
.fetch_one(&self.db)
.await?;
let mut resp = false;
if let Some(x) = res.exists {
if x {
resp = true;
}
}
Ok(resp)
}
/// Register an mCaptcha instance
pub async fn mcaptcha_register_instance(&self, url: &str) -> ServiceResult<String> {
let secret = get_random(32);
sqlx::query!(
"INSERT INTO survey_mcaptcha_hostname (url, secret) VALUES ($1, $2)",
url,
&secret,
)
.execute(&self.db)
.await?;
Ok(secret)
}
/// Update the secret of an mCaptcha instance
pub async fn mcaptcha_update_secret(&self, url: &str) -> ServiceResult<String> {
let secret = get_random(32);
sqlx::query!(
"UPDATE survey_mcaptcha_hostname set secret = $1 WHERE url = $2",
&secret,
url
)
.execute(&self.db)
.await?;
Ok(secret)
}
/// Authenticate an mCaptcha instance and return its URL
pub async fn mcaptcha_authenticate_and_get_url(
&self,
secret: &str,
) -> ServiceResult<Url> {
struct U {
url: String,
}
let res = sqlx::query!(
"SELECT EXISTS (
SELECT
url
FROM
survey_mcaptcha_hostname
WHERE secret = $1
)",
secret
)
.fetch_one(&self.db)
.await?;
if !match res.exists {
Some(true) => true,
_ => false,
} {
return Err(ServiceError::WrongPassword);
}
let url = sqlx::query_as!(
U,
"SELECT
url
FROM
survey_mcaptcha_hostname
WHERE
secret = $1; ",
secret
)
.fetch_one(&self.db)
.await?;
Ok(Url::parse(&url.url).unwrap())
}
/// Delete mCaptcha instance from database
pub async fn mcaptcha_delete_mcaptcha_instance(
&self,
url: &str,
secret: &str,
) -> ServiceResult<()> {
sqlx::query!(
"DELETE FROM survey_mcaptcha_hostname WHERE secret = $1 AND url =$2",
secret,
url
)
.execute(&self.db)
.await?;
Ok(())
}
/// Delete mCaptcha camapign from database
pub async fn mcaptcha_delete_mcaptcha_campaign(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
let res = sqlx::query!(
"DELETE FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT
ID
FROM
survey_mcaptcha_hostname
WHERE
secret = $2
)",
&campaign_str,
secret
)
.execute(&self.db)
.await?;
Ok(())
}
/// Check if an mCaptcha instance campaign is registered on DB
pub async fn mcaptcha_campaign_is_registered(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
) -> ServiceResult<bool> {
let campaign_str = campaign_id.to_string();
let res = sqlx::query!(
"SELECT EXISTS (
SELECT
ID
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT
ID
FROM
survey_mcaptcha_hostname
WHERE
secret = $2
)
)",
&campaign_str,
secret
)
.fetch_one(&self.db)
.await?;
let mut resp = false;
if let Some(x) = res.exists {
if x {
resp = true;
}
}
Ok(resp)
}
/// Register an mCaptcha instance campaign on DB
pub async fn mcaptcha_register_campaign(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
let public_id = uuid::Uuid::new_v4();
sqlx::query!(
"INSERT INTO
survey_mcaptcha_campaign (campaign_id, public_id, url_id)
VALUES ($1, $2, (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3));",
&campaign_str,
&public_id.to_string(),
secret,
)
.execute(&self.db)
.await?;
Ok(())
}
/// Register an mCaptcha instance campaign on DB
pub async fn mcaptcha_get_campaign_public_id(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
) -> ServiceResult<uuid::Uuid> {
let campaign_str = campaign_id.to_string();
struct S {
public_id: String,
}
let res = sqlx::query_as!(
S,
"SELECT
public_id
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2);",
&campaign_str,
secret,
)
.fetch_one(&self.db)
.await?;
Ok(uuid::Uuid::parse_str(&res.public_id).unwrap())
}
/// Get an mCaptcha instance campaign checkpoint
pub async fn mcaptcha_get_checkpoint(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
) -> ServiceResult<usize> {
let campaign_str = campaign_id.to_string();
struct CheckPoint {
synced_till: i32,
}
let checkpoint = sqlx::query_as!(
CheckPoint,
"SELECT
synced_till
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2
);",
&campaign_str,
secret
)
.fetch_one(&self.db)
.await?;
let checkpoint = checkpoint.synced_till as usize;
Ok(checkpoint)
}
/// Set an mCaptcha instance campaign checkpoint
pub async fn mcaptcha_set_checkpoint(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
checkpoint: usize,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
sqlx::query!(
"UPDATE
survey_mcaptcha_campaign
SET
synced_till = $1
WHERE
campaign_id = $2
AND
url_id = (
SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $3
)
",
checkpoint as i32,
&campaign_str,
secret
)
.execute(&self.db)
.await?;
Ok(())
}
/// Store mCaptcha instance campaign analytics
pub async fn mcaptcha_insert_analytics(
&self,
campaign_id: &uuid::Uuid,
secret: &str,
r: &PerformanceAnalytics,
) -> ServiceResult<()> {
let campaign_str = campaign_id.to_string();
sqlx::query!(
"INSERT INTO
survey_mcaptcha_analytics (
campaign_id, time, difficulty_factor, worker_type
)
VALUES ((
SELECT
ID
FROM
survey_mcaptcha_campaign
WHERE
campaign_id = $1
AND
url_id = (
SELECT ID FROM survey_mcaptcha_hostname WHERE secret = $2
)
), $3, $4, $5
);",
&campaign_str,
secret,
r.time as i32,
r.difficulty_factor as i32,
&r.worker_type,
)
.execute(&self.db)
.await?;
Ok(())
}
/// fetch PoW analytics
pub async fn mcaptcha_analytics_fetch(
&self,
public_id: &uuid::Uuid,
limit: usize,
offset: usize,
) -> ServiceResult<Vec<PerformanceAnalytics>> {
let public_id_str = public_id.to_string();
struct P {
id: i32,
time: i32,
difficulty_factor: i32,
worker_type: String,
}
impl From<P> for PerformanceAnalytics {
fn from(v: P) -> Self {
Self {
time: v.time as u32,
difficulty_factor: v.difficulty_factor as u32,
worker_type: v.worker_type,
id: v.id as usize,
}
}
}
let mut c = sqlx::query_as!(
P,
"SELECT id, time, difficulty_factor, worker_type FROM survey_mcaptcha_analytics
WHERE
campaign_id = (
SELECT
ID FROM survey_mcaptcha_campaign
WHERE
public_id = $1
)
ORDER BY ID
OFFSET $2 LIMIT $3
",
&public_id_str,
offset as i32,
limit as i32
)
.fetch_all(&self.db)
.await?;
let mut res = Vec::with_capacity(c.len());
for i in c.drain(0..) {
res.push(i.into())
}
Ok(res)
}
}
#[cfg(test)]
mod tests {
use crate::{mcaptcha::PerformanceAnalytics, tests::*};
use url::Url;
#[actix_rt::test]
async fn test_db_mcaptcha_works() {
let url = Url::parse("http://test_add_campaign.example").unwrap();
let data = get_test_data().await;
let url_str = url.to_string();
if data.mcaptcha_url_exists(&url_str).await.unwrap() {
let secret = data.mcaptcha_update_secret(&url_str).await.unwrap();
data.mcaptcha_delete_mcaptcha_instance(&url_str, &secret)
.await
.unwrap();
}
assert!(!data.mcaptcha_url_exists(&url_str).await.unwrap());
let secret = data.mcaptcha_register_instance(&url_str).await.unwrap();
assert!(data.mcaptcha_url_exists(&url_str).await.unwrap());
let secret2 = data.mcaptcha_update_secret(&url_str).await.unwrap();
assert_ne!(secret2, secret);
let secret = secret2;
assert_eq!(
data.mcaptcha_authenticate_and_get_url(&secret)
.await
.unwrap(),
url
);
let uuid = uuid::Uuid::new_v4();
if data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap()
{
data.mcaptcha_delete_mcaptcha_campaign(&uuid, &secret)
.await
.unwrap();
}
assert!(!data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap());
data.mcaptcha_register_campaign(&uuid, &secret)
.await
.unwrap();
assert!(data
.mcaptcha_campaign_is_registered(&uuid, &secret)
.await
.unwrap());
assert_eq!(
data.mcaptcha_get_checkpoint(&uuid, &secret).await.unwrap(),
0
);
data.mcaptcha_set_checkpoint(&uuid, &secret, 1)
.await
.unwrap();
assert_eq!(
data.mcaptcha_get_checkpoint(&uuid, &secret).await.unwrap(),
1
);
let analytics = PerformanceAnalytics {
id: 1,
time: 1,
difficulty_factor: 1,
worker_type: "foo".to_string(),
};
data.mcaptcha_insert_analytics(&uuid, &secret, &analytics)
.await
.unwrap();
let public_id = data
.mcaptcha_get_campaign_public_id(&uuid, &secret)
.await
.unwrap();
let db_analytics = data
.mcaptcha_analytics_fetch(&public_id, 50, 0)
.await
.unwrap();
assert_eq!(db_analytics.len(), 1);
assert_eq!(db_analytics[0].time, analytics.time);
assert_eq!(
db_analytics[0].difficulty_factor,
analytics.difficulty_factor
);
assert_eq!(db_analytics[0].worker_type, analytics.worker_type);
assert_eq!(
data.mcaptcha_analytics_fetch(&public_id, 50, 1)
.await
.unwrap(),
vec![]
);
}
}