rm domains and added authorization check at all endpoints

This commit is contained in:
realaravinth
2021-04-09 16:52:05 +05:30
parent 646a92b28f
commit 4e7e8da574
12 changed files with 201 additions and 638 deletions

View File

@@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_domains_verified (
name VARCHAR(100) PRIMARY KEY NOT NULL UNIQUE,
owner_id INTEGER references mcaptcha_users(ID) ON DELETE CASCADE NOT NULL
);

View File

@@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS mcaptcha_config ( CREATE TABLE IF NOT EXISTS mcaptcha_config (
config_id SERIAL PRIMARY KEY NOT NULL, config_id SERIAL PRIMARY KEY NOT NULL,
domain_name varchar(100) NOT NULL references mcaptcha_domains_verified(name) ON DELETE CASCADE, user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
key varchar(100) NOT NULL UNIQUE, key varchar(100) NOT NULL UNIQUE,
name varchar(100) NOT NULL UNIQUE, name varchar(100) DEFAULT NULL,
duration integer NOT NULL DEFAULT 30 duration integer NOT NULL DEFAULT 30
); );

View File

@@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_domains_unverified (
name VARCHAR(100) PRIMARY KEY NOT NULL,
owner_id INTEGER references mcaptcha_users(ID) ON DELETE CASCADE NOT NULL,
verified BOOLEAN DEFAULT NULL,
verification_challenge VARCHAR(32) NOT NULL
);

View File

@@ -1,324 +0,0 @@
/*
* Copyright (C) 2021 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 actix_identity::Identity;
use actix_web::{client::Client, post, web, HttpResponse, Responder};
//use awc::Client;
use serde::{Deserialize, Serialize};
use url::Url;
use super::{get_random, is_authenticated};
use crate::errors::*;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Domain {
pub name: String,
}
#[post("/api/v1/mcaptcha/domain/add")]
pub async fn add_domain(
payload: web::Json<Domain>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let url = Url::parse(&payload.name)?;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
let user = id.identity().unwrap();
let challenge = get_random(32);
let res = sqlx::query!(
"INSERT INTO mcaptcha_domains_unverified
(name, owner_id, verification_challenge) VALUES
($1, (SELECT ID FROM mcaptcha_users WHERE name = ($2) ), $3);",
host,
user,
challenge
)
.execute(&data.db)
.await;
match res {
Err(e) => Err(dup_error(e, ServiceError::HostnameTaken)),
Ok(_) => Ok(HttpResponse::Ok()),
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Challenge {
verification_challenge: String,
}
#[post("/api/v1/mcaptcha/domain/verify/challenge/get")]
pub async fn get_challenge(
payload: web::Json<Domain>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let url = Url::parse(&payload.name)?;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
let user = id.identity().unwrap();
let res = sqlx::query_as!(
Challenge,
"SELECT verification_challenge
FROM mcaptcha_domains_unverified where
name = $1 AND owner_id = (SELECT ID from mcaptcha_users where name = $2)",
host,
user,
)
.fetch_one(&data.db)
.await?;
Ok(HttpResponse::Ok().json(res))
}
#[post("/api/v1/mcaptcha/domain/verify/challenge/prove")]
pub async fn verify(
payload: web::Json<Domain>,
data: web::Data<Data>,
client: web::Data<Client>,
id: Identity,
) -> ServiceResult<impl Responder> {
use crate::VERIFICATION_PATH;
use futures::{future::TryFutureExt, try_join};
is_authenticated(&id)?;
let url = Url::parse(&payload.name)?;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
let user = id.identity().unwrap();
let challenge_fut = sqlx::query_as!(
Challenge,
"SELECT verification_challenge
FROM mcaptcha_domains_unverified where
name = $1 AND owner_id = (SELECT ID from mcaptcha_users where name = $2)",
&host,
&user,
)
.fetch_one(&data.db)
.map_err(|e| {
let r: ServiceError = e.into();
r
});
let res_fut = client
.get(format!("{}{}", url.to_string(), VERIFICATION_PATH))
.send()
.map_err(|e| {
let r: ServiceError = e.into();
r
});
let (challenge, mut server_res) = try_join!(challenge_fut, res_fut)?;
let server_resp: Challenge = server_res
.json()
.await
.map_err(|_| return ServiceError::ChallengeCourruption)?;
if server_resp.verification_challenge == challenge.verification_challenge {
sqlx::query!(
"INSERT INTO mcaptcha_domains_verified (name, owner_id) VALUES
($1, (SELECT ID from mcaptcha_users WHERE name = $2))",
&host,
&user
)
.execute(&data.db)
.await?;
// TODO delete staging unverified
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::ChallengeVerificationFailure)
}
}
#[post("/api/v1/mcaptcha/domain/delete")]
pub async fn delete_domain(
payload: web::Json<Domain>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let url = Url::parse(&payload.name)?;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
sqlx::query!(
"DELETE FROM mcaptcha_domains_verified WHERE name = ($1)",
host,
)
.execute(&data.db)
.await?;
// TODO check running actors and delete
// if domain(api_key) matches mcaptcha actor id
Ok(HttpResponse::Ok())
}
// Workflow:
// 1. Sign up
// 2. Sign in
// 3. Add domain(DNS TXT record verification? / put string at path)
// 4. Create token
// 5. Add levels
// 6. Update duration
// 7. Start syatem
#[cfg(test)]
mod tests {
use actix_web::http::{header, StatusCode};
use actix_web::test;
use super::*;
use crate::api::v1::services as v1_services;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn add_domains_work() {
const NAME: &str = "testuserdomainn";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserdomain@a.com";
const DOMAIN: &str = "http://example.com";
const ADD_URL: &str = "/api/v1/mcaptcha/domain/add";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(NAME, EMAIL, PASSWORD).await;
// 1. add domain
let (data, _, signin_resp) = add_domain_util(NAME, PASSWORD, DOMAIN).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
let mut domain = Domain {
name: DOMAIN.into(),
};
// 2. duplicate domain
bad_post_req_test(
NAME,
PASSWORD,
ADD_URL,
&domain,
ServiceError::HostnameTaken,
StatusCode::BAD_REQUEST,
)
.await;
// 3. delete domain
let del_domain_resp = test::call_service(
&mut app,
post_request!(&domain, "/api/v1/mcaptcha/domain/delete")
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(del_domain_resp.status(), StatusCode::OK);
// 4. not a URL test for adding domain
domain.name = "testing".into();
bad_post_req_test(
NAME,
PASSWORD,
ADD_URL,
&domain,
ServiceError::NotAUrl,
StatusCode::BAD_REQUEST,
)
.await;
}
#[actix_rt::test]
async fn domain_verification_works() {
use crate::api::v1::tests::*;
use std::sync::mpsc;
use std::thread;
const NAME: &str = "testdomainveri";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "domainverification@a.com";
const DOMAIN: &str = "http://localhost:18001";
const IP: &str = "localhost:18001";
const CHALLENGE_GET: &str = "/api/v1/mcaptcha/domain/verify/challenge/get";
const CHALLENGE_VERIFY: &str = "/api/v1/mcaptcha/domain/verify/challenge/prove";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
actix_rt::System::new("").block_on(server(IP, tx));
});
let srv = rx.recv().unwrap();
let client = Client::new();
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = test::init_service(
App::new()
.wrap(get_identity_service())
.configure(v1_services)
.data(data.clone())
.data(client.clone()),
)
.await;
let domain = Domain {
name: DOMAIN.into(),
};
let add_domain_resp = test::call_service(
&mut app,
post_request!(&domain, "/api/v1/mcaptcha/domain/add")
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_domain_resp.status(), StatusCode::OK);
let get_challenge_resp = test::call_service(
&mut app,
post_request!(&domain, CHALLENGE_GET)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_challenge_resp.status(), StatusCode::OK);
let challenge: Challenge = test::read_body_json(get_challenge_resp).await;
client
.post(format!("{}/{}/", DOMAIN, VERIFICATION_PATH))
.send_json(&challenge)
.await
.unwrap();
let verify_challenge_resp = test::call_service(
&mut app,
post_request!(&domain, CHALLENGE_VERIFY)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(verify_challenge_resp.status(), StatusCode::OK);
srv.stop(true).await;
}
}

View File

@@ -20,12 +20,13 @@ use actix_web::{post, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::is_authenticated; use super::is_authenticated;
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::errors::*; use crate::errors::*;
use crate::Data; use crate::Data;
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct UpdateDuration { pub struct UpdateDuration {
pub token_name: String, pub key: String,
pub duration: i32, pub duration: i32,
} }
@@ -36,13 +37,15 @@ pub async fn update_duration(
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
if payload.duration > 0 { if payload.duration > 0 {
sqlx::query!( sqlx::query!(
"UPDATE mcaptcha_config set duration = $1 WHERE "UPDATE mcaptcha_config set duration = $1
name = $2;", WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
&payload.duration, &payload.duration,
&payload.token_name, &payload.key,
&username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -68,17 +71,19 @@ pub struct GetDuration {
#[post("/api/v1/mcaptcha/domain/token/duration/get")] #[post("/api/v1/mcaptcha/domain/token/duration/get")]
pub async fn get_duration( pub async fn get_duration(
payload: web::Json<GetDuration>, payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
let duration = sqlx::query_as!( let duration = sqlx::query_as!(
GetDurationResp, GetDurationResp,
"SELECT duration FROM mcaptcha_config WHERE "SELECT duration FROM mcaptcha_config
name = $1;", WHERE key = $1 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)",
&payload.token, &payload.key,
&username,
) )
.fetch_one(&data.db) .fetch_one(&data.db)
.await?; .await?;
@@ -100,8 +105,6 @@ mod tests {
const NAME: &str = "testuserduration"; const NAME: &str = "testuserduration";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserduration@a.com"; const EMAIL: &str = "testuserduration@a.com";
const DOMAIN: &str = "http://duration.example.com";
const TOKEN_NAME: &str = "duration_routes_token";
const GET_URL: &str = "/api/v1/mcaptcha/domain/token/duration/get"; const GET_URL: &str = "/api/v1/mcaptcha/domain/token/duration/get";
const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update"; const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update";
@@ -111,24 +114,20 @@ mod tests {
} }
register_and_signin(NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
let update = UpdateDuration { let update = UpdateDuration {
token_name: TOKEN_NAME.into(), key: token_key.key.clone(),
duration: 40, duration: 40,
}; };
let get = GetDuration {
token: TOKEN_NAME.into(),
};
// check default // check default
let get_level_resp = test::call_service( let get_level_resp = test::call_service(
&mut app, &mut app,
post_request!(&get, GET_URL) post_request!(&token_key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
@@ -149,7 +148,7 @@ mod tests {
assert_eq!(update_duration.status(), StatusCode::OK); assert_eq!(update_duration.status(), StatusCode::OK);
let get_level_resp = test::call_service( let get_level_resp = test::call_service(
&mut app, &mut app,
post_request!(&get, GET_URL) post_request!(&token_key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )

View File

@@ -21,6 +21,7 @@ use m_captcha::{defense::Level, DefenseBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::is_authenticated; use super::is_authenticated;
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::errors::*; use crate::errors::*;
use crate::Data; use crate::Data;
@@ -28,12 +29,12 @@ use crate::Data;
pub struct AddLevels { pub struct AddLevels {
pub levels: Vec<Level>, pub levels: Vec<Level>,
// name is config_name // name is config_name
pub name: String, pub key: String,
} }
// TODO try for non-existent token names // TODO try for non-existent token names
#[post("/api/v1/mcaptcha/domain/token/levels/add")] #[post("/api/v1/mcaptcha/levels/add")]
pub async fn add_levels( pub async fn add_levels(
payload: web::Json<AddLevels>, payload: web::Json<AddLevels>,
data: web::Data<Data>, data: web::Data<Data>,
@@ -41,6 +42,7 @@ pub async fn add_levels(
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let mut defense = DefenseBuilder::default(); let mut defense = DefenseBuilder::default();
let username = id.identity().unwrap();
for level in payload.levels.iter() { for level in payload.levels.iter() {
defense.add_level(level.clone())?; defense.add_level(level.clone())?;
@@ -55,10 +57,16 @@ pub async fn add_levels(
"INSERT INTO mcaptcha_levels ( "INSERT INTO mcaptcha_levels (
difficulty_factor, difficulty_factor,
visitor_threshold, visitor_threshold,
config_id) VALUES ($1, $2, (SELECT config_id FROM mcaptcha_config WHERE name = ($3) ));", config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE
key = ($3) AND user_id = (
SELECT ID FROM mcaptcha_users WHERE name = $4
)));",
difficulty_factor, difficulty_factor,
visitor_threshold, visitor_threshold,
&payload.name, &payload.key,
&username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -67,13 +75,14 @@ pub async fn add_levels(
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[post("/api/v1/mcaptcha/domain/token/levels/update")] #[post("/api/v1/mcaptcha/levels/update")]
pub async fn update_levels( pub async fn update_levels(
payload: web::Json<AddLevels>, payload: web::Json<AddLevels>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
let mut defense = DefenseBuilder::default(); let mut defense = DefenseBuilder::default();
for level in payload.levels.iter() { for level in payload.levels.iter() {
@@ -88,9 +97,13 @@ pub async fn update_levels(
sqlx::query!( sqlx::query!(
"DELETE FROM mcaptcha_levels "DELETE FROM mcaptcha_levels
WHERE config_id = ( WHERE config_id = (
SELECT config_id FROM mcaptcha_config where name = ($1) SELECT config_id FROM mcaptcha_config where key = ($1)
AND user_id = (
SELECT ID from mcaptcha_users WHERE name = $2
)
)", )",
&payload.name, &payload.key,
&username
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -102,10 +115,17 @@ pub async fn update_levels(
"INSERT INTO mcaptcha_levels ( "INSERT INTO mcaptcha_levels (
difficulty_factor, difficulty_factor,
visitor_threshold, visitor_threshold,
config_id) VALUES ($1, $2, (SELECT config_id FROM mcaptcha_config WHERE name = ($3) ));", config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND
user_id = (
SELECT ID from mcaptcha_users WHERE name = $4
)
));",
difficulty_factor, difficulty_factor,
visitor_threshold, visitor_threshold,
&payload.name, &payload.key,
&username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -114,23 +134,26 @@ pub async fn update_levels(
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[post("/api/v1/mcaptcha/domain/token/levels/delete")] #[post("/api/v1/mcaptcha/levels/delete")]
pub async fn delete_levels( pub async fn delete_levels(
payload: web::Json<AddLevels>, payload: web::Json<AddLevels>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
for level in payload.levels.iter() { for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32; let difficulty_factor = level.difficulty_factor as i32;
sqlx::query!( sqlx::query!(
"DELETE FROM mcaptcha_levels WHERE "DELETE FROM mcaptcha_levels WHERE
config_id = ( config_id = (
SELECT config_id FROM mcaptcha_config WHERE name = ($1) SELECT config_id FROM mcaptcha_config WHERE key = $1 AND
user_id = (SELECT ID from mcaptcha_users WHERE name = $3)
) AND difficulty_factor = ($2);", ) AND difficulty_factor = ($2);",
&payload.name, &payload.key,
difficulty_factor, difficulty_factor,
&username
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -139,20 +162,16 @@ pub async fn delete_levels(
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[derive(Deserialize, Serialize)] #[post("/api/v1/mcaptcha/levels/get")]
pub struct GetLevels {
pub token: String,
}
#[post("/api/v1/mcaptcha/domain/token/levels/get")]
pub async fn get_levels( pub async fn get_levels(
payload: web::Json<GetLevels>, payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
let levels = get_levels_util(&payload.token, &data).await?; let levels = get_levels_util(&payload.key, &username, &data).await?;
Ok(HttpResponse::Ok().json(levels)) Ok(HttpResponse::Ok().json(levels))
} }
@@ -168,14 +187,16 @@ pub struct I32Levels {
visitor_threshold: i32, visitor_threshold: i32,
} }
async fn get_levels_util(name: &str, data: &Data) -> ServiceResult<Vec<I32Levels>> { async fn get_levels_util(key: &str, username: &str, data: &Data) -> ServiceResult<Vec<I32Levels>> {
let levels = sqlx::query_as!( let levels = sqlx::query_as!(
I32Levels, I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = ( config_id = (
SELECT config_id FROM mcaptcha_config WHERE name = ($1) SELECT config_id FROM mcaptcha_config WHERE key = ($1)
AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)
);", );",
name key,
&username
) )
.fetch_all(&data.db) .fetch_all(&data.db)
.await?; .await?;
@@ -198,12 +219,10 @@ mod tests {
const NAME: &str = "testuserlevelroutes"; const NAME: &str = "testuserlevelroutes";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserlevelrouts@a.com"; const EMAIL: &str = "testuserlevelrouts@a.com";
const DOMAIN: &str = "http://level.example.com"; const ADD_URL: &str = "/api/v1/mcaptcha/levels/add";
const TOKEN_NAME: &str = "level_routes_work"; const UPDATE_URL: &str = "/api/v1/mcaptcha/levels/update";
const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/levels/add"; const DEL_URL: &str = "/api/v1/mcaptcha/levels/delete";
const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/levels/update"; const GET_URL: &str = "/api/v1/mcaptcha/levels/get";
const DEL_URL: &str = "/api/v1/mcaptcha/domain/token/levels/delete";
const GET_URL: &str = "/api/v1/mcaptcha/domain/token/levels/get";
let l1 = Level { let l1 = Level {
difficulty_factor: 50, difficulty_factor: 50,
@@ -214,14 +233,6 @@ mod tests {
visitor_threshold: 500, visitor_threshold: 500,
}; };
let levels = vec![l1, l2]; let levels = vec![l1, l2];
let add_level = AddLevels {
levels: levels.clone(),
name: TOKEN_NAME.into(),
};
let get_level = GetLevels {
token: TOKEN_NAME.into(),
};
{ {
let data = Data::new().await; let data = Data::new().await;
@@ -229,10 +240,15 @@ mod tests {
} }
register_and_signin(NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; let (data, _, signin_resp, key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
let add_level = AddLevels {
levels: levels.clone(),
key: key.key.clone(),
};
// 1. add level // 1. add level
let add_token_resp = test::call_service( let add_token_resp = test::call_service(
&mut app, &mut app,
@@ -246,7 +262,7 @@ mod tests {
// 2. get level // 2. get level
let get_level_resp = test::call_service( let get_level_resp = test::call_service(
&mut app, &mut app,
post_request!(&get_level, GET_URL) post_request!(&key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
@@ -268,7 +284,7 @@ mod tests {
let levels = vec![l1, l2]; let levels = vec![l1, l2];
let add_level = AddLevels { let add_level = AddLevels {
levels: levels.clone(), levels: levels.clone(),
name: TOKEN_NAME.into(), key: key.key.clone(),
}; };
let add_token_resp = test::call_service( let add_token_resp = test::call_service(
&mut app, &mut app,
@@ -280,7 +296,7 @@ mod tests {
assert_eq!(add_token_resp.status(), StatusCode::OK); assert_eq!(add_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service( let get_level_resp = test::call_service(
&mut app, &mut app,
post_request!(&get_level, GET_URL) post_request!(&key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
@@ -301,7 +317,7 @@ mod tests {
let levels = vec![l1, l2]; let levels = vec![l1, l2];
let add_level = AddLevels { let add_level = AddLevels {
levels: levels.clone(), levels: levels.clone(),
name: TOKEN_NAME.into(), key: key.key.clone(),
}; };
let add_token_resp = test::call_service( let add_token_resp = test::call_service(
&mut app, &mut app,
@@ -313,7 +329,7 @@ mod tests {
assert_eq!(add_token_resp.status(), StatusCode::OK); assert_eq!(add_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service( let get_level_resp = test::call_service(
&mut app, &mut app,
post_request!(&get_level, GET_URL) post_request!(&key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )

View File

@@ -18,7 +18,6 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{post, web, HttpResponse, Responder}; use actix_web::{post, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url;
use super::{get_random, is_authenticated}; use super::{get_random, is_authenticated};
use crate::errors::*; use crate::errors::*;
@@ -26,68 +25,58 @@ use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MCaptchaID { pub struct MCaptchaID {
pub name: String, pub name: Option<String>,
pub domain: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MCaptchaDetails { pub struct MCaptchaDetails {
pub name: String, pub name: Option<String>,
pub key: String, pub key: String,
} }
#[post("/api/v1/mcaptcha/domain/token/add")] #[post("/api/v1/mcaptcha/add")]
pub async fn add_mcaptcha( pub async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> {
payload: web::Json<MCaptchaID>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
let key = get_random(32); let key = get_random(32);
let url = Url::parse(&payload.domain)?;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
let res = sqlx::query!( let res = sqlx::query!(
"INSERT INTO mcaptcha_config "INSERT INTO mcaptcha_config
(name, key, domain_name) (key, user_id)
VALUES ($1, $2, ( VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2))",
SELECT name FROM mcaptcha_domains_verified WHERE name = ($3)))",
&payload.name,
&key, &key,
&host, &username,
) )
.execute(&data.db) .execute(&data.db)
.await; .await;
match res { match res {
Err(e) => Err(dup_error(e, ServiceError::TokenNameTaken)), Err(e) => {
println!("{}", &e);
Err(dup_error(e, ServiceError::TokenNameTaken))
}
Ok(_) => { Ok(_) => {
let resp = MCaptchaDetails { let resp = MCaptchaDetails { key, name: None };
key,
name: payload.into_inner().name,
};
Ok(HttpResponse::Ok().json(resp)) Ok(HttpResponse::Ok().json(resp))
} }
} }
} }
#[post("/api/v1/mcaptcha/domain/token/update")] #[post("/api/v1/mcaptcha/update/key")]
pub async fn update_token( pub async fn update_token(
payload: web::Json<MCaptchaID>, payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
use std::borrow::Cow; use std::borrow::Cow;
is_authenticated(&id)?; is_authenticated(&id)?;
let url = Url::parse(&payload.domain)?; let username = id.identity().unwrap();
let mut key; let mut key;
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
loop { loop {
key = get_random(32); key = get_random(32);
let res = update_token_helper(&key, &payload.name, &host, &data).await; let res = update_token_helper(&key, &payload.key, &username, &data).await;
if res.is_ok() { if res.is_ok() {
break; break;
} else { } else {
@@ -111,37 +100,36 @@ pub async fn update_token(
async fn update_token_helper( async fn update_token_helper(
key: &str, key: &str,
name: &str, old_key: &str,
host: &str, username: &str,
data: &Data, data: &Data,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!( sqlx::query!(
"UPDATE mcaptcha_config SET key = $1 "UPDATE mcaptcha_config SET key = $1
WHERE name = $2 AND domain_name = $3", WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
&key, &key,
&name, &old_key,
&host, &username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
Ok(()) Ok(())
} }
#[post("/api/v1/mcaptcha/domain/token/get")] #[post("/api/v1/mcaptcha/get")]
pub async fn get_token( pub async fn get_token(
payload: web::Json<MCaptchaID>, payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let url = Url::parse(&payload.domain)?; let username = id.identity().unwrap();
let host = url.host_str().ok_or(ServiceError::NotAUrl)?;
let res = match sqlx::query_as!( let res = match sqlx::query_as!(
MCaptchaDetails, MCaptchaDetails,
"SELECT key, name from mcaptcha_config WHERE name = $1 AND domain_name = $2", "SELECT key, name from mcaptcha_config
&payload.name, WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&host, &payload.key,
&username,
) )
.fetch_one(&data.db) .fetch_one(&data.db)
.await .await
@@ -157,16 +145,20 @@ pub async fn get_token(
Ok(HttpResponse::Ok().json(res)) Ok(HttpResponse::Ok().json(res))
} }
#[post("/api/v1/mcaptcha/domain/token/delete")] #[post("/api/v1/mcaptcha/delete")]
pub async fn delete_mcaptcha( pub async fn delete_mcaptcha(
payload: web::Json<MCaptchaID>, payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>, data: web::Data<Data>,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
is_authenticated(&id)?; is_authenticated(&id)?;
let username = id.identity().unwrap();
sqlx::query!( sqlx::query!(
"DELETE FROM mcaptcha_config WHERE name = ($1)", "DELETE FROM mcaptcha_config
&payload.name, WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&payload.key,
&username,
) )
.execute(&data.db) .execute(&data.db)
.await?; .await?;
@@ -197,10 +189,8 @@ mod tests {
const NAME: &str = "testusermcaptcha"; const NAME: &str = "testusermcaptcha";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testusermcaptcha@a.com"; const EMAIL: &str = "testusermcaptcha@a.com";
const DOMAIN: &str = "http://mcaptcha.example.com"; const ADD_URL: &str = "/api/v1/mcaptcha/add";
const TOKEN_NAME: &str = "add_mcaptcha_works_token"; const DEL_URL: &str = "/api/v1/mcaptcha/delete";
const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/add";
const DEL_URL: &str = "/api/v1/mcaptcha/domain/token/delete";
{ {
let data = Data::new().await; let data = Data::new().await;
@@ -209,42 +199,18 @@ mod tests {
// 1. add mcaptcha token // 1. add mcaptcha token
register_and_signin(NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
let mut domain = MCaptchaID { // let mut domain = MCaptchaID {
domain: DOMAIN.into(), // name: TOKEN_NAME.into(),
name: TOKEN_NAME.into(), // };
};
// 2. add duplicate mcaptha
bad_post_req_test(
NAME,
PASSWORD,
ADD_URL,
&domain,
ServiceError::TokenNameTaken,
StatusCode::BAD_REQUEST,
)
.await;
// 4. not a URL test for adding domain
domain.domain = "testing".into();
bad_post_req_test(
NAME,
PASSWORD,
ADD_URL,
&domain,
ServiceError::NotAUrl,
StatusCode::BAD_REQUEST,
)
.await;
// 4. delete token // 4. delete token
let del_token = test::call_service( let del_token = test::call_service(
&mut app, &mut app,
post_request!(&domain, DEL_URL) post_request!(&token_key, DEL_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
@@ -257,10 +223,8 @@ mod tests {
const NAME: &str = "updateusermcaptcha"; const NAME: &str = "updateusermcaptcha";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testupdateusermcaptcha@a.com"; const EMAIL: &str = "testupdateusermcaptcha@a.com";
const DOMAIN: &str = "http://update-mcaptcha.example.com"; const UPDATE_URL: &str = "/api/v1/mcaptcha/update/key";
const TOKEN_NAME: &str = "get_update_mcaptcha_works_token"; const GET_URL: &str = "/api/v1/mcaptcha/get";
const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/update";
const GET_URL: &str = "/api/v1/mcaptcha/domain/token/get";
{ {
let data = Data::new().await; let data = Data::new().await;
@@ -269,18 +233,14 @@ mod tests {
// 1. add mcaptcha token // 1. add mcaptcha token
register_and_signin(NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
let mut domain = MCaptchaID { // 2. update token key
domain: DOMAIN.into(),
name: TOKEN_NAME.into(),
};
let update_token_resp = test::call_service( let update_token_resp = test::call_service(
&mut app, &mut app,
post_request!(&domain, UPDATE_URL) post_request!(&token_key, UPDATE_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
@@ -288,22 +248,25 @@ mod tests {
assert_eq!(update_token_resp.status(), StatusCode::OK); assert_eq!(update_token_resp.status(), StatusCode::OK);
let updated_token: MCaptchaDetails = test::read_body_json(update_token_resp).await; let updated_token: MCaptchaDetails = test::read_body_json(update_token_resp).await;
// get token key with updated key
let get_token_resp = test::call_service( let get_token_resp = test::call_service(
&mut app, &mut app,
post_request!(&domain, GET_URL) post_request!(&updated_token, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )
.await; .await;
assert_eq!(get_token_resp.status(), StatusCode::OK); assert_eq!(get_token_resp.status(), StatusCode::OK);
let get_token_key: MCaptchaDetails = test::read_body_json(get_token_resp).await;
// check if they match
let mut get_token_key: MCaptchaDetails = test::read_body_json(get_token_resp).await;
assert_eq!(get_token_key.key, updated_token.key); assert_eq!(get_token_key.key, updated_token.key);
domain.name = "https://batsense.net".into(); get_token_key.key = "nonexistent".into();
let get_nonexistent_token_resp = test::call_service( let get_nonexistent_token_resp = test::call_service(
&mut app, &mut app,
post_request!(&domain, GET_URL) post_request!(&get_token_key, GET_URL)
.cookie(cookies.clone()) .cookie(cookies.clone())
.to_request(), .to_request(),
) )

View File

@@ -15,7 +15,6 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
pub mod domains;
pub mod duration; pub mod duration;
pub mod levels; pub mod levels;
pub mod mcaptcha; pub mod mcaptcha;

View File

@@ -34,13 +34,6 @@ pub fn services(cfg: &mut ServiceConfig) {
cfg.service(auth::username_exists); cfg.service(auth::username_exists);
cfg.service(auth::email_exists); cfg.service(auth::email_exists);
// mcaptcha
// domain
cfg.service(mcaptcha::domains::add_domain);
cfg.service(mcaptcha::domains::delete_domain);
cfg.service(mcaptcha::domains::verify);
cfg.service(mcaptcha::domains::get_challenge);
// mcaptcha // mcaptcha
cfg.service(mcaptcha::mcaptcha::add_mcaptcha); cfg.service(mcaptcha::mcaptcha::add_mcaptcha);
cfg.service(mcaptcha::mcaptcha::delete_mcaptcha); cfg.service(mcaptcha::mcaptcha::delete_mcaptcha);

View File

@@ -1,77 +0,0 @@
/*
* Copyright (C) 2021 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 log::info;
use std::collections::HashMap;
use std::sync::mpsc;
use std::sync::{Arc, RwLock};
use actix_web::{dev::Server, middleware, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
/*
* Simple KV Server that stores a json of with schema
* `Challenge` at path /{key}/ on POST and emits on GET
*/
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Challenge {
verification_challenge: String,
}
pub async fn server(ip: &str, tx: mpsc::Sender<Server>) {
pretty_env_logger::init();
let srv = HttpServer::new(move || {
let store: UtilKVServer = Arc::new(RwLock::new(HashMap::new()));
App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::NormalizePath::default())
.data(store)
.route("/{key}/", web::post().to(util_server_add))
.route("/{key}/", web::get().to(util_server_retrive))
})
.bind(ip)
.unwrap()
.run();
tx.send(srv.clone()).unwrap();
}
type UtilKVServer = Arc<RwLock<HashMap<String, Challenge>>>;
#[cfg(not(tarpaulin_include))]
async fn util_server_retrive(
key: web::Path<String>,
data: web::Data<UtilKVServer>,
) -> impl Responder {
let key = key.into_inner();
let store = data.read().unwrap();
let resp = store.get(&key).unwrap();
info!("[kv-server] retrive: key :{}, value: {:?}", key, resp);
HttpResponse::Ok().json(resp)
}
#[cfg(not(tarpaulin_include))]
async fn util_server_add(
key: web::Path<String>,
payload: web::Json<Challenge>,
data: web::Data<UtilKVServer>,
) -> impl Responder {
info!("[kv-server] cache: key :{}, value: {:?}", key, payload);
let mut store = data.write().unwrap();
store.insert(key.into_inner(), payload.into_inner());
HttpResponse::Ok()
}

View File

@@ -16,6 +16,3 @@
*/ */
mod auth; mod auth;
mod kvserver;
pub use kvserver::server;

View File

@@ -7,6 +7,7 @@ use serde::Serialize;
use super::*; use super::*;
use crate::api::v1::auth::{Login, Register}; use crate::api::v1::auth::{Login, Register};
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::api::v1::services as v1_services; use crate::api::v1::services as v1_services;
use crate::data::Data; use crate::data::Data;
use crate::errors::*; use crate::errors::*;
@@ -30,6 +31,10 @@ pub async fn delete_user(name: &str, data: &Data) {
#[macro_export] #[macro_export]
macro_rules! post_request { macro_rules! post_request {
($uri:expr) => {
test::TestRequest::post().uri($uri)
};
($serializable:expr, $uri:expr) => { ($serializable:expr, $uri:expr) => {
test::TestRequest::post() test::TestRequest::post()
.uri($uri) .uri($uri)
@@ -96,79 +101,81 @@ pub async fn signin<'a>(name: &'a str, password: &str) -> (data::Data, Login, Se
(data, creds, signin_resp) (data, creds, signin_resp)
} }
/// register and signin and domain ///// register and signin and domain
/// bypasses domain verification, use with care ///// bypasses domain verification, use with care
pub async fn add_domain_util( //pub async fn add_domain_util(
// name: &str,
// password: &str,
// domain: &str,
//) -> (data::Data, Login, ServiceResponse) {
// use crate::api::v1::mcaptcha::domains::Domain;
// use url::Url;
//
// let (data, creds, signin_resp) = signin(name, password).await;
// let cookies = get_cookie!(signin_resp);
// let mut app = get_app!(data).await;
//
// // 1. add domain
// let add_domain = Domain {
// name: domain.into(),
// };
//
// let add_domain_resp = test::call_service(
// &mut app,
// post_request!(&add_domain, "/api/v1/mcaptcha/domain/add")
// .cookie(cookies.clone())
// .to_request(),
// )
// .await;
// assert_eq!(add_domain_resp.status(), StatusCode::OK);
//
// // verification work around
// let url = Url::parse(domain).unwrap();
// let host = url.host_str().unwrap();
// sqlx::query!(
// "INSERT INTO mcaptcha_domains_verified (name, owner_id) VALUES
// ($1, (SELECT ID from mcaptcha_users WHERE name = $2))",
// &host,
// &name
// )
// .execute(&data.db)
// .await
// .unwrap();
//
// (data, creds, signin_resp)
//}
pub async fn add_token_util(
name: &str, name: &str,
password: &str, password: &str,
domain: &str, ) -> (data::Data, Login, ServiceResponse, MCaptchaDetails) {
) -> (data::Data, Login, ServiceResponse) { // use crate::api::v1::mcaptcha::mcaptcha::MCaptchaID;
use crate::api::v1::mcaptcha::domains::Domain;
use url::Url; const ADD_URL: &str = "/api/v1/mcaptcha/add";
let (data, creds, signin_resp) = signin(name, password).await; let (data, creds, signin_resp) = signin(name, password).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
// 1. add domain // // 1. add mcaptcha token
let add_domain = Domain { // let domain = MCaptchaID {
name: domain.into(), // name: token_name.into(),
}; // };
let add_domain_resp = test::call_service(
&mut app,
post_request!(&add_domain, "/api/v1/mcaptcha/domain/add")
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_domain_resp.status(), StatusCode::OK);
// verification work around
let url = Url::parse(domain).unwrap();
let host = url.host_str().unwrap();
sqlx::query!(
"INSERT INTO mcaptcha_domains_verified (name, owner_id) VALUES
($1, (SELECT ID from mcaptcha_users WHERE name = $2))",
&host,
&name
)
.execute(&data.db)
.await
.unwrap();
(data, creds, signin_resp)
}
pub async fn add_token_util(
name: &str,
password: &str,
domain: &str,
token_name: &str,
) -> (data::Data, Login, ServiceResponse) {
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaID;
const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/add";
let (data, creds, signin_resp) = add_domain_util(name, password, domain).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
// 1. add mcaptcha token
let domain = MCaptchaID {
domain: domain.into(),
name: token_name.into(),
};
let add_token_resp = test::call_service( let add_token_resp = test::call_service(
&mut app, &mut app,
post_request!(&domain, ADD_URL) post_request!(ADD_URL).cookie(cookies.clone()).to_request(),
.cookie(cookies.clone())
.to_request(),
) )
.await; .await;
// let status = add_token_resp.status();
// let txt: ErrorToResponse = test::read_body_json(add_token_resp).await;
// println!("{:?}", txt.error);
//
assert_eq!(add_token_resp.status(), StatusCode::OK); assert_eq!(add_token_resp.status(), StatusCode::OK);
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
(data, creds, signin_resp) // assert_eq!(status, StatusCode::OK);
(data, creds, signin_resp, token_key)
} }
/// pub duplicate test /// pub duplicate test