feat & chore: update actix-web and deps and use actix-auth-middleware for guarding auth routes
This commit is contained in:
parent
81f3f5e450
commit
7ff66c551d
21 changed files with 877 additions and 949 deletions
1143
Cargo.lock
generated
1143
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
15
Cargo.toml
15
Cargo.toml
|
@ -22,10 +22,10 @@ name = "tests-migrate"
|
|||
path = "./src/tests-migrate.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.0-beta.9"
|
||||
actix-web = "4.0.1"
|
||||
actix-identity = "0.4.0-beta.2"
|
||||
actix-session = "0.5.0-beta.2"
|
||||
actix-http = "3.0.0-beta.8"
|
||||
actix-session = { version = "0.6.1", features = ["cookie-session"]}
|
||||
actix-http = "3.0.4"
|
||||
actix-rt = "2"
|
||||
actix-cors = "0.6.0-beta.2"
|
||||
actix-service = "2.0.0"
|
||||
|
@ -40,7 +40,7 @@ sqlx = { version = "0.5.9", features = [ "runtime-actix-rustls", "postgres", "ti
|
|||
|
||||
argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"}
|
||||
|
||||
derive_builder = "0.10"
|
||||
derive_builder = "0.11"
|
||||
validator = { version = "0.14", features = ["derive"]}
|
||||
derive_more = "0.99"
|
||||
|
||||
|
@ -69,6 +69,13 @@ sailfish = "0.3.2"
|
|||
|
||||
#tokio = "1.11.0"
|
||||
|
||||
[dependencies.actix-auth-middleware]
|
||||
branch = "v4"
|
||||
features = ["actix_identity_backend"]
|
||||
git = "https://github.com/realaravinth/actix-auth-middleware"
|
||||
version = "0.2"
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { version = "0.5.9", features = [ "runtime-actix-rustls", "uuid", "postgres", "time", "offline" ] }
|
||||
#serde_yaml = "0.8.17"
|
||||
|
|
260
sqlx-data.json
260
sqlx-data.json
|
@ -1,82 +1,82 @@
|
|||
{
|
||||
"db": "PostgreSQL",
|
||||
"03c9789e83a398bed96354924a0e63ccaa97bec667fda1b8277bb9afda9a6fcd": {
|
||||
"query": "DELETE \n FROM survey_campaigns \n WHERE \n user_id = (\n SELECT \n ID \n FROM \n survey_admins \n WHERE \n name = $1\n )\n AND\n id = ($2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "DELETE \n FROM survey_campaigns \n WHERE \n user_id = (\n SELECT \n ID \n FROM \n survey_admins \n WHERE \n name = $1\n )\n AND\n id = ($2)"
|
||||
},
|
||||
"0d22134cc5076304b7895827f006ee8269cc500f400114a7472b83f0f1c568b5": {
|
||||
"query": "INSERT INTO survey_admins \n (name , password, secret) VALUES ($1, $2, $3)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO survey_admins \n (name , password, secret) VALUES ($1, $2, $3)"
|
||||
},
|
||||
"1373df097fa0e58b23a374753318ae53a44559aa0e7eb64680185baf1c481723": {
|
||||
"query": "SELECT password FROM survey_admins WHERE name = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "password",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT password FROM survey_admins WHERE name = ($1)"
|
||||
},
|
||||
"19686bfe8772cbc6831d46d18994e2b9aa40c7181eae9a31e51451cce95f04e8": {
|
||||
"query": "SELECT name, password FROM survey_admins WHERE email = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "password",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT name, password FROM survey_admins WHERE email = ($1)"
|
||||
},
|
||||
"1b7e17bfc949fa97e8dec1f95e35a02bcf3aa1aa72a1f6f6c8884e885fc3b953": {
|
||||
"query": "insert into survey_admins \n (name , password, email, secret) values ($1, $2, $3, $4)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
|
@ -84,152 +84,152 @@
|
|||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "insert into survey_admins \n (name , password, email, secret) values ($1, $2, $3, $4)"
|
||||
},
|
||||
"2ccaecfee4d2f29ef5278188b304017719720aa986d680d4727a1facbb869c7a": {
|
||||
"query": "DELETE FROM survey_admins WHERE name = ($1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "DELETE FROM survey_admins WHERE name = ($1)"
|
||||
},
|
||||
"43b3e771f38bf8059832169227705be06a28925af1b3799ffef5371d511fd138": {
|
||||
"query": "\n INSERT INTO survey_users (created_at, id) VALUES($1, $2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamptz",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO survey_users (created_at, id) VALUES($1, $2)"
|
||||
},
|
||||
"536541ecf2e1c0403c74b6e2e09b42b73a7741ae4a348ff539ac410022e03ace": {
|
||||
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE name = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
null
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE name = $1)"
|
||||
},
|
||||
"55dde28998a6d12744806035f0a648494a403c7d09ea3caf91bf54869a81aa73": {
|
||||
"query": "UPDATE survey_admins set password = $1\n WHERE name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "UPDATE survey_admins set password = $1\n WHERE name = $2"
|
||||
},
|
||||
"58ec3b8f98c27e13ec2732f8ee23f6eb9845ac5d9fd97b1e5c9f2eed4b1f5693": {
|
||||
"query": "SELECT name \n FROM survey_campaigns\n WHERE \n id = $1\n AND\n user_id = (SELECT ID from survey_admins WHERE name = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT name \n FROM survey_campaigns\n WHERE \n id = $1\n AND\n user_id = (SELECT ID from survey_admins WHERE name = $2)"
|
||||
},
|
||||
"683707dbc847b37c58c29aaad0d1a978c9fe0657da13af99796e4461134b5a43": {
|
||||
"query": "UPDATE survey_admins set email = $1\n WHERE name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "UPDATE survey_admins set email = $1\n WHERE name = $2"
|
||||
},
|
||||
"6a26daa84578aed2b2085697cb8358ed7c0a50ba9597fd387b4b09b0a8a154db": {
|
||||
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE email = $1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
}
|
||||
},
|
||||
"70cc7bfc9b6ff5b68db70c069c0947d51bfc4a53cedc020016ee25ff98586c93": {
|
||||
"query": "SELECT \n name, id\n FROM \n survey_campaigns \n WHERE\n user_id = (\n SELECT \n ID\n FROM \n survey_admins\n WHERE\n name = $1\n )",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"query": "SELECT EXISTS (SELECT 1 from survey_admins WHERE email = $1)"
|
||||
},
|
||||
"70cc7bfc9b6ff5b68db70c069c0947d51bfc4a53cedc020016ee25ff98586c93": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 1,
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT \n name, id\n FROM \n survey_campaigns \n WHERE\n user_id = (\n SELECT \n ID\n FROM \n survey_admins\n WHERE\n name = $1\n )"
|
||||
},
|
||||
"82feafc36533144e49ba374c8c47ca4aa0d6558a9803778ad28cfa7b62382c3e": {
|
||||
"query": "\n INSERT INTO survey_campaigns (\n user_id, ID, name, difficulties, created_at\n ) VALUES(\n (SELECT id FROM survey_admins WHERE name = $1),\n $2, $3, $4, $5\n );",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
|
@ -238,82 +238,82 @@
|
|||
"Int4Array",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO survey_campaigns (\n user_id, ID, name, difficulties, created_at\n ) VALUES(\n (SELECT id FROM survey_admins WHERE name = $1),\n $2, $3, $4, $5\n );"
|
||||
},
|
||||
"8320dda2b3e107d1451fdfb35eb2a4b8e97364e7b1b74ffe4d6913faf132fb61": {
|
||||
"query": "SELECT ID \n FROM survey_responses \n WHERE \n user_id = $1 \n AND \n device_software_recognised = $2;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT ID \n FROM survey_responses \n WHERE \n user_id = $1 \n AND \n device_software_recognised = $2;"
|
||||
},
|
||||
"9cdade613ce724631cc3f187510758ee0929e93ff3f8ce81fe35594756644246": {
|
||||
"query": "SELECT difficulties FROM survey_campaigns WHERE id = $1;",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "difficulties",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int4Array"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT difficulties FROM survey_campaigns WHERE id = $1;"
|
||||
},
|
||||
"a721cfa249acf328c2f29c4cf8c2aeba1a635bcf49d18ced5474caa10b7cae4f": {
|
||||
"query": "INSERT INTO survey_benches \n (resp_id, difficulty, duration) \n VALUES ($1, $2, $3);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Int4",
|
||||
"Float4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO survey_benches \n (resp_id, difficulty, duration) \n VALUES ($1, $2, $3);"
|
||||
},
|
||||
"ab951c5c318174c6538037947c2f52c61bcfe5e5be1901379b715e77f5214dd2": {
|
||||
"query": "UPDATE survey_admins set secret = $1\n WHERE name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "UPDATE survey_admins set secret = $1\n WHERE name = $2"
|
||||
},
|
||||
"b4cd1e5240de1968c8b6d56672cec639b22f41ebf2754dadbf00efe0948c7e68": {
|
||||
"query": "INSERT INTO survey_responses (\n user_id, \n campaign_id,\n device_user_provided,\n device_software_recognised,\n threads\n ) VALUES ($1, $2, $3, $4, $5);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
|
@ -322,55 +322,55 @@
|
|||
"Varchar",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO survey_responses (\n user_id, \n campaign_id,\n device_user_provided,\n device_software_recognised,\n threads\n ) VALUES ($1, $2, $3, $4, $5);"
|
||||
},
|
||||
"c757589ef26a005e3285e7ab20d8a44c4f2e1cb125f8db061dd198cc380bf807": {
|
||||
"query": "UPDATE survey_admins set name = $1\n WHERE name = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "UPDATE survey_admins set name = $1\n WHERE name = $2"
|
||||
},
|
||||
"e9cf5d6d8c9e8327d5c809d47a14a933f324e267f1e7dbb48e1caf1c021adc3f": {
|
||||
"query": "SELECT secret FROM survey_admins WHERE name = ($1)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "secret",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "SELECT secret FROM survey_admins WHERE name = ($1)"
|
||||
},
|
||||
"fcdc5fe5d496eb516c805e64ec96d9626b74ab33cd6e75e5a08ae88967403b72": {
|
||||
"query": "INSERT INTO survey_response_tokens \n (resp_id, user_id, id)\n VALUES ($1, $2, $3);",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int4",
|
||||
"Uuid",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO survey_response_tokens \n (resp_id, user_id, id)\n VALUES ($1, $2, $3);"
|
||||
}
|
||||
}
|
|
@ -25,8 +25,8 @@ use crate::errors::*;
|
|||
use crate::AppData;
|
||||
|
||||
pub mod routes {
|
||||
use crate::middleware::auth::GetLoginRoute;
|
||||
use url::Url;
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
pub struct Auth {
|
||||
pub logout: &'static str,
|
||||
pub login: &'static str,
|
||||
|
|
|
@ -352,10 +352,10 @@ mod tests {
|
|||
use crate::api::v1::bench::Submission;
|
||||
use crate::data::Data;
|
||||
use crate::errors::*;
|
||||
use crate::middleware::auth::GetLoginRoute;
|
||||
use crate::tests::*;
|
||||
use crate::*;
|
||||
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
use actix_web::{http::header, test};
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
* 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_auth_middleware::*;
|
||||
use actix_web::web::ServiceConfig;
|
||||
|
||||
pub mod account;
|
||||
|
@ -23,6 +24,7 @@ pub mod campaigns;
|
|||
mod tests;
|
||||
|
||||
pub use super::{get_random, get_uuid, RedirectQuery};
|
||||
use crate::api::v1::bench::SURVEY_USER_ID;
|
||||
|
||||
pub fn services(cfg: &mut ServiceConfig) {
|
||||
auth::services(cfg);
|
||||
|
@ -30,12 +32,8 @@ pub fn services(cfg: &mut ServiceConfig) {
|
|||
campaigns::services(cfg);
|
||||
}
|
||||
|
||||
pub fn get_admin_check_login() -> crate::CheckLogin<auth::routes::Auth> {
|
||||
use crate::middleware::auth::*;
|
||||
CheckLogin::new(
|
||||
crate::V1_API_ROUTES.admin.auth,
|
||||
AuthenticatedSession::ActixIdentity,
|
||||
)
|
||||
pub fn get_admin_check_login() -> Authentication<auth::routes::Auth> {
|
||||
Authentication::with_identity(super::ROUTES.admin.auth)
|
||||
}
|
||||
|
||||
pub mod routes {
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_auth_middleware::*;
|
||||
use actix_session::Session;
|
||||
use actix_web::{dev::Payload, HttpRequest};
|
||||
use actix_web::{http, web, HttpResponse, Responder};
|
||||
use futures::future::try_join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -32,7 +34,7 @@ pub const SURVEY_USER_ID: &str = "survey_user_id";
|
|||
|
||||
pub mod routes {
|
||||
|
||||
use crate::middleware::auth::GetLoginRoute;
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
pub struct Benches {
|
||||
pub submit: &'static str,
|
||||
|
@ -173,14 +175,27 @@ pub struct SubmissionProof {
|
|||
pub proof: String,
|
||||
}
|
||||
|
||||
pub fn get_check_login() -> crate::CheckLogin<routes::Benches> {
|
||||
use crate::middleware::auth::*;
|
||||
CheckLogin::new(
|
||||
crate::V1_API_ROUTES.benches,
|
||||
AuthenticatedSession::ActixSession,
|
||||
fn is_session_authenticated(r: &HttpRequest, mut pl: &mut Payload) -> bool {
|
||||
use actix_web::FromRequest;
|
||||
matches!(
|
||||
Session::from_request(&r, &mut pl).into_inner().map(|x| {
|
||||
let val = x.get::<String>(SURVEY_USER_ID);
|
||||
println!("{:#?}", val);
|
||||
val
|
||||
}),
|
||||
Ok(Ok(Some(_)))
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_check_login() -> Authentication<routes::Benches> {
|
||||
Authentication::new(crate::V1_API_ROUTES.benches, is_session_authenticated)
|
||||
}
|
||||
//
|
||||
// pub fn get_auth_middleware() -> Authentication<routes::Routes> {
|
||||
// Authentication::with_identity(V1_API_ROUTES)
|
||||
// }
|
||||
//}
|
||||
|
||||
#[my_codegen::post(
|
||||
path = "crate::V1_API_ROUTES.benches.submit",
|
||||
wrap = "get_check_login()"
|
||||
|
|
22
src/main.rs
22
src/main.rs
|
@ -18,6 +18,7 @@ use std::env;
|
|||
use std::sync::Arc;
|
||||
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::{
|
||||
error::InternalError, http::StatusCode, middleware as actix_middleware,
|
||||
web::JsonConfig, App, HttpServer,
|
||||
|
@ -28,7 +29,6 @@ use log::info;
|
|||
mod api;
|
||||
mod data;
|
||||
mod errors;
|
||||
mod middleware;
|
||||
mod pages;
|
||||
mod settings;
|
||||
mod static_assets;
|
||||
|
@ -38,7 +38,6 @@ mod tests;
|
|||
|
||||
pub use crate::data::Data;
|
||||
pub use api::v1::ROUTES as V1_API_ROUTES;
|
||||
pub use middleware::auth::CheckLogin;
|
||||
pub use pages::routes::ROUTES as PAGES;
|
||||
pub use settings::Settings;
|
||||
pub use static_assets::static_files::assets;
|
||||
|
@ -136,16 +135,17 @@ pub fn get_json_err() -> JsonConfig {
|
|||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn get_survey_session() -> actix_session::CookieSession {
|
||||
pub fn get_survey_session() -> actix_session::SessionMiddleware<CookieSessionStore> {
|
||||
use actix_web::cookie::Key;
|
||||
let cookie_secret = &SETTINGS.server.cookie_secret2;
|
||||
actix_session::CookieSession::signed(cookie_secret.as_bytes())
|
||||
.lazy(true)
|
||||
.domain(&SETTINGS.server.domain)
|
||||
.name("survey-id")
|
||||
.http_only(true)
|
||||
.path("/")
|
||||
.max_age(30 * 60)
|
||||
.secure(false)
|
||||
let key = Key::from(cookie_secret.as_bytes());
|
||||
SessionMiddleware::builder(CookieSessionStore::default(), key)
|
||||
.cookie_domain(Some(SETTINGS.server.domain.clone()))
|
||||
.cookie_name("survey-id".into())
|
||||
.cookie_path("/".to_string())
|
||||
.cookie_secure(false)
|
||||
.cookie_http_only(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
|
|
|
@ -1,245 +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/>.
|
||||
*/
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::api::v1::bench::SURVEY_USER_ID;
|
||||
use actix_http::body::AnyBody;
|
||||
use actix_identity::Identity;
|
||||
use actix_service::{Service, Transform};
|
||||
use actix_session::Session;
|
||||
use actix_web::dev::{ServiceRequest, ServiceResponse};
|
||||
use actix_web::{http, Error, FromRequest, HttpResponse};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AuthenticatedSession {
|
||||
ActixIdentity,
|
||||
ActixSession,
|
||||
}
|
||||
|
||||
use futures::future::{ok, Either, Ready};
|
||||
|
||||
pub trait GetLoginRoute {
|
||||
fn get_login_route(&self, src: Option<&str>) -> String;
|
||||
}
|
||||
|
||||
pub struct CheckLogin<T: GetLoginRoute> {
|
||||
login: Rc<T>,
|
||||
session_type: AuthenticatedSession,
|
||||
}
|
||||
|
||||
impl<T: GetLoginRoute> CheckLogin<T> {
|
||||
pub fn new(login: T, session_type: AuthenticatedSession) -> Self {
|
||||
let login = Rc::new(login);
|
||||
Self {
|
||||
login,
|
||||
session_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, GT> Transform<S, ServiceRequest> for CheckLogin<GT>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
GT: GetLoginRoute,
|
||||
{
|
||||
type Response = ServiceResponse<AnyBody>;
|
||||
type Error = Error;
|
||||
type Transform = CheckLoginMiddleware<S, GT>;
|
||||
type InitError = ();
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(CheckLoginMiddleware {
|
||||
service,
|
||||
login: self.login.clone(),
|
||||
session_type: self.session_type.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
pub struct CheckLoginMiddleware<S, GT> {
|
||||
service: S,
|
||||
login: Rc<GT>,
|
||||
session_type: AuthenticatedSession,
|
||||
}
|
||||
|
||||
impl<S, GT> Service<ServiceRequest> for CheckLoginMiddleware<S, GT>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<AnyBody>, Error = Error>,
|
||||
S::Future: 'static,
|
||||
GT: GetLoginRoute,
|
||||
{
|
||||
type Response = ServiceResponse<AnyBody>;
|
||||
type Error = Error;
|
||||
type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;
|
||||
|
||||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let (r, mut pl) = req.into_parts();
|
||||
let mut is_authenticated = || match self.session_type {
|
||||
AuthenticatedSession::ActixSession => matches!(
|
||||
Session::from_request(&r, &mut pl)
|
||||
.into_inner()
|
||||
.map(|x| x.get::<String>(SURVEY_USER_ID)),
|
||||
Ok(Ok(Some(_)))
|
||||
),
|
||||
|
||||
AuthenticatedSession::ActixIdentity => matches!(
|
||||
Identity::from_request(&r, &mut pl)
|
||||
.into_inner()
|
||||
.map(|x| x.identity()),
|
||||
Ok(Some(_))
|
||||
),
|
||||
};
|
||||
if is_authenticated() {
|
||||
let req = ServiceRequest::from_parts(r, pl);
|
||||
Either::Left(self.service.call(req))
|
||||
} else {
|
||||
let path = r.uri().path_and_query().map(|path| path.as_str());
|
||||
let path = self.login.get_login_route(path);
|
||||
let req = ServiceRequest::from_parts(r, pl);
|
||||
Either::Right(ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.insert_header((http::header::LOCATION, path))
|
||||
.finish(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use url::Url;
|
||||
|
||||
use crate::api::v1::bench::Submission;
|
||||
use crate::data::Data;
|
||||
use crate::middleware::auth::GetLoginRoute;
|
||||
use crate::tests::*;
|
||||
use crate::*;
|
||||
|
||||
use actix_web::{http::header, test};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn auth_middleware_works() {
|
||||
fn make_uri(path: &str, queries: &Option<Vec<(&str, &str)>>) -> String {
|
||||
let mut url = Url::parse("http://x/").unwrap();
|
||||
let final_path;
|
||||
url.set_path(path);
|
||||
|
||||
if let Some(queries) = queries {
|
||||
{
|
||||
let mut query_pairs = url.query_pairs_mut();
|
||||
queries.iter().for_each(|(k, v)| {
|
||||
query_pairs.append_pair(k, v);
|
||||
});
|
||||
}
|
||||
|
||||
final_path = format!("{}?{}", url.path(), url.query().unwrap());
|
||||
} else {
|
||||
final_path = url.path().to_string();
|
||||
}
|
||||
final_path
|
||||
}
|
||||
|
||||
const NAME: &str = "testmiddlewareuser";
|
||||
const EMAIL: &str = "testuserupda@testmiddlewareuser.com";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const DEVICE_USER_PROVIDED: &str = "foo";
|
||||
const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
|
||||
const THREADS: i32 = 4;
|
||||
let queries = Some(vec![
|
||||
("foo", "bar"),
|
||||
("src", "/x/y/z"),
|
||||
("with_q", "/a/b/c/?goo=x"),
|
||||
]);
|
||||
|
||||
{
|
||||
let data = Data::new().await;
|
||||
delete_user(NAME, &data).await;
|
||||
}
|
||||
let (data, _creds, signin_resp) =
|
||||
register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let survey = get_survey_user(data.clone()).await;
|
||||
let survey_cookie = get_cookie!(survey);
|
||||
|
||||
let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await;
|
||||
|
||||
let bench_submit_route =
|
||||
V1_API_ROUTES.benches.submit_route(&campaign.campaign_id);
|
||||
let bench_routes = vec![
|
||||
(&bench_submit_route, queries.clone()),
|
||||
(&bench_submit_route, None),
|
||||
];
|
||||
|
||||
let app = get_app!(data).await;
|
||||
|
||||
// let campaign_routes = vec![
|
||||
// (Some(V1_API_ROUTES.camp.submit), queries.clone()),
|
||||
// (None, None),
|
||||
// (Some(V1_API_ROUTES.benches.submit), None),
|
||||
// ];
|
||||
|
||||
let bench_submit_payload = Submission {
|
||||
device_user_provided: DEVICE_USER_PROVIDED.into(),
|
||||
device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(),
|
||||
threads: THREADS,
|
||||
benches: BENCHES.clone(),
|
||||
};
|
||||
|
||||
for (from, query) in bench_routes.iter() {
|
||||
let route = make_uri(from, query);
|
||||
let signin_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&bench_submit_payload, &route).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(signin_resp.status(), StatusCode::FOUND);
|
||||
|
||||
let redirect_to = V1_API_ROUTES.benches.get_login_route(Some(&route));
|
||||
let headers = signin_resp.headers();
|
||||
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to);
|
||||
|
||||
let add_feedback_resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&bench_submit_payload, &route)
|
||||
.cookie(survey_cookie.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(add_feedback_resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
||||
// let signin_resp = test::call_service(
|
||||
// &app,
|
||||
// test::TestRequest::get()
|
||||
// .uri(V1_API_ROUTES.benches.get_login_route(redirect_to).as_ref().unwrap())
|
||||
// .to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(signin_resp.status(), StatusCode::FOUND);
|
||||
// let headers = signin_resp.headers();
|
||||
// assert_eq!(
|
||||
// headers.get(header::LOCATION).unwrap(),
|
||||
// redirect_to.as_ref().unwrap()
|
||||
// )
|
||||
//
|
||||
}
|
|
@ -1,18 +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/>.
|
||||
*/
|
||||
|
||||
pub mod auth;
|
|
@ -55,7 +55,7 @@ lazy_static! {
|
|||
pub async fn join() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*INDEX)
|
||||
.body(&*INDEX.as_str())
|
||||
}
|
||||
|
||||
#[my_codegen::post(path = "PAGES.auth.join")]
|
||||
|
|
|
@ -58,7 +58,7 @@ lazy_static! {
|
|||
pub async fn login() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*INDEX)
|
||||
.body(&*INDEX.as_str())
|
||||
}
|
||||
|
||||
#[post(path = "PAGES.auth.login")]
|
||||
|
|
|
@ -26,7 +26,7 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
|||
}
|
||||
|
||||
pub mod routes {
|
||||
use crate::middleware::auth::GetLoginRoute;
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
use url::Url;
|
||||
|
||||
pub struct Auth {
|
||||
|
|
|
@ -58,11 +58,11 @@ async fn error(path: web::Path<usize>) -> impl Responder {
|
|||
let resp = match path.into_inner() {
|
||||
500 => HttpResponse::InternalServerError()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*INTERNAL_SERVER_ERROR_BODY),
|
||||
.body(&*INTERNAL_SERVER_ERROR_BODY.as_str()),
|
||||
|
||||
_ => HttpResponse::InternalServerError()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*UNKNOWN_ERROR_BODY),
|
||||
.body(&*UNKNOWN_ERROR_BODY.as_str()),
|
||||
};
|
||||
|
||||
resp
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
*
|
||||
* 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_auth_middleware::*;
|
||||
use actix_web::web::ServiceConfig;
|
||||
|
||||
pub mod auth;
|
||||
|
@ -30,9 +30,8 @@ pub fn services(cfg: &mut ServiceConfig) {
|
|||
errors::services(cfg);
|
||||
}
|
||||
|
||||
pub fn get_page_check_login() -> crate::CheckLogin<auth::routes::Auth> {
|
||||
use crate::middleware::auth::*;
|
||||
CheckLogin::new(crate::PAGES.auth, AuthenticatedSession::ActixIdentity)
|
||||
pub fn get_page_check_login() -> Authentication<auth::routes::Auth> {
|
||||
Authentication::with_identity(crate::PAGES.auth)
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
|
|
|
@ -54,6 +54,6 @@ pub async fn bench(path: web::Path<String>) -> PageResult<impl Responder> {
|
|||
Err(_) => Err(PageError::PageDoesntExist),
|
||||
Ok(_) => Ok(HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*BENCH)),
|
||||
.body(&*BENCH.as_str())),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -102,7 +102,7 @@ pub async fn home(data: AppData, id: Identity) -> impl Responder {
|
|||
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&page)
|
||||
.body(page)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -60,7 +60,7 @@ lazy_static! {
|
|||
pub async fn new_campaign() -> impl Responder {
|
||||
HttpResponse::Ok()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&*INDEX)
|
||||
.body(&*INDEX.as_str())
|
||||
}
|
||||
|
||||
#[post(
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
* 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_auth_middleware::GetLoginRoute;
|
||||
|
||||
use super::auth::routes::Auth;
|
||||
use super::errors::routes::Errors;
|
||||
use super::panel::routes::Panel;
|
||||
|
@ -57,6 +59,25 @@ impl Routes {
|
|||
}
|
||||
}
|
||||
|
||||
impl GetLoginRoute for Routes {
|
||||
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||
if let Some(redirect_to) = src {
|
||||
// uri::Builder::new().path_and_query(
|
||||
format!(
|
||||
"{}?redirect_to={}",
|
||||
self.auth.join.to_string(),
|
||||
urlencoding::encode(redirect_to)
|
||||
)
|
||||
// let mut url: Uri = self.register.parse().unwrap();
|
||||
// url.qu
|
||||
// url.query_pairs_mut()
|
||||
// .append_pair("redirect_to", redirect_to);
|
||||
} else {
|
||||
self.auth.join.to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
use std::borrow::Cow;
|
||||
|
||||
use actix_web::body::Body;
|
||||
use actix_web::body::BoxBody;
|
||||
use actix_web::{get, http::header, web, HttpResponse, Responder};
|
||||
use log::debug;
|
||||
use mime_guess::from_path;
|
||||
|
@ -61,9 +61,9 @@ struct Asset;
|
|||
fn handle_assets(path: &str) -> HttpResponse {
|
||||
match Asset::get(path) {
|
||||
Some(content) => {
|
||||
let body: Body = match content.data {
|
||||
Cow::Borrowed(bytes) => bytes.into(),
|
||||
Cow::Owned(bytes) => bytes.into(),
|
||||
let body: BoxBody = match content.data {
|
||||
Cow::Borrowed(bytes) => BoxBody::new(bytes),
|
||||
Cow::Owned(bytes) => BoxBody::new(bytes),
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
|
@ -91,9 +91,9 @@ struct Favicons;
|
|||
fn handle_favicons(path: &str) -> HttpResponse {
|
||||
match Favicons::get(path) {
|
||||
Some(content) => {
|
||||
let body: Body = match content.data {
|
||||
Cow::Borrowed(bytes) => bytes.into(),
|
||||
Cow::Owned(bytes) => bytes.into(),
|
||||
let body: BoxBody = match content.data {
|
||||
Cow::Borrowed(bytes) => BoxBody::new(bytes),
|
||||
Cow::Owned(bytes) => BoxBody::new(bytes),
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
|
|
22
src/tests.rs
22
src/tests.rs
|
@ -19,7 +19,13 @@ use std::sync::Arc;
|
|||
|
||||
use actix_web::cookie::Cookie;
|
||||
use actix_web::test;
|
||||
use actix_web::{dev::ServiceResponse, error::ResponseError, http::StatusCode};
|
||||
use actix_web::{
|
||||
body::{BoxBody, EitherBody},
|
||||
dev::ServiceResponse,
|
||||
error::ResponseError,
|
||||
http::StatusCode,
|
||||
};
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
@ -110,7 +116,7 @@ pub async fn register_and_signin(
|
|||
name: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
) -> (Arc<data::Data>, Login, ServiceResponse) {
|
||||
) -> (Arc<Data>, Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||
register(name, email, password).await;
|
||||
signin(name, password).await
|
||||
}
|
||||
|
@ -136,7 +142,10 @@ pub async fn register(name: &str, email: &str, password: &str) {
|
|||
}
|
||||
|
||||
/// signin util
|
||||
pub async fn signin(name: &str, password: &str) -> (Arc<Data>, Login, ServiceResponse) {
|
||||
pub async fn signin(
|
||||
name: &str,
|
||||
password: &str,
|
||||
) -> (Arc<Data>, Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||
let data = Data::new().await;
|
||||
let app = get_app!(data.clone()).await;
|
||||
|
||||
|
@ -226,7 +235,7 @@ pub async fn create_new_campaign(
|
|||
uuid
|
||||
}
|
||||
|
||||
pub async fn get_survey_user(data: Arc<Data>) -> ServiceResponse {
|
||||
pub async fn get_survey_user(data: Arc<Data>) -> ServiceResponse<EitherBody<BoxBody>> {
|
||||
let app = get_app!(data).await;
|
||||
let signin_resp = test::call_service(
|
||||
&app,
|
||||
|
@ -306,6 +315,11 @@ pub async fn submit_bench(
|
|||
post_request!(&payload, &route).cookie(cookies).to_request(),
|
||||
)
|
||||
.await;
|
||||
if add_feedback_resp.status() != StatusCode::OK {
|
||||
let headers = add_feedback_resp.headers();
|
||||
println!("{:#?}", headers);
|
||||
}
|
||||
|
||||
assert_eq!(add_feedback_resp.status(), StatusCode::OK);
|
||||
|
||||
let proof: SubmissionProof = test::read_body_json(add_feedback_resp).await;
|
||||
|
|
Loading…
Reference in a new issue