diff --git a/Cargo.lock b/Cargo.lock index 2c366964..26a19a08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1179,6 +1179,7 @@ dependencies = [ "log", "m_captcha", "pretty_env_logger", + "rand 0.8.3", "serde 1.0.124", "serde_json", "sqlx", diff --git a/Cargo.toml b/Cargo.toml index 66d7bf03..b13901c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,8 @@ path = "./src/tests-migrate.rs" [dependencies] actix-web = "3" actix = "0.10" +actix-identity = "0.3" +actix-http = "2.2" sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres" ] } argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds", commit = "61f2d1d" } @@ -40,11 +42,11 @@ log = "0.4" lazy_static = "1.4" -actix-identity = "0.3" -actix-http = "2.2" m_captcha = { version = "0.1.0", git = "https://github.com/mCaptcha/mCaptcha", tag = "0.1.0" } +rand = "0.8" + [dev-dependencies] actix-rt = "1" diff --git a/migrations/20210310122339_mcaptcha_domains.sql b/migrations/20210310122339_mcaptcha_domains.sql index f5854cdd..df482b4f 100644 --- a/migrations/20210310122339_mcaptcha_domains.sql +++ b/migrations/20210310122339_mcaptcha_domains.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS mcaptcha_domains ( name VARCHAR(100) PRIMARY KEY NOT NULL UNIQUE, - ID INTEGER references mcaptcha_users(ID) + ID INTEGER references mcaptcha_users(ID) NOT NULL ); diff --git a/migrations/20210310122617_mcaptcha_config.sql b/migrations/20210310122617_mcaptcha_config.sql index b00ca28f..7f260ba7 100644 --- a/migrations/20210310122617_mcaptcha_config.sql +++ b/migrations/20210310122617_mcaptcha_config.sql @@ -2,5 +2,5 @@ CREATE TABLE IF NOT EXISTS mcaptcha_config ( config_id SERIAL PRIMARY KEY NOT NULL, ID INTEGER references mcaptcha_users(ID), key VARCHAR(100) NOT NULL UNIQUE, - duration INTEGER NOT NULL + duration INTEGER NOT NULL DEFAULT 30 ); diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs index cc673ad4..8c580554 100644 --- a/src/api/v1/auth.rs +++ b/src/api/v1/auth.rs @@ -48,15 +48,19 @@ pub async fn signup( let username = data.creds.username(&payload.username)?; let hash = data.creds.password(&payload.password)?; data.creds.email(Some(&payload.email))?; - sqlx::query!( + let res = sqlx::query!( "INSERT INTO mcaptcha_users (name , password, email) VALUES ($1, $2, $3)", username, hash, &payload.email ) .execute(&data.db) - .await?; - Ok(HttpResponse::Ok()) + .await; + + match res { + Err(e) => Err(dup_error(e, ServiceError::UsernameTaken)), + Ok(_) => Ok(HttpResponse::Ok()), + } } #[post("/api/v1/signin")] @@ -129,6 +133,8 @@ pub async fn delete_account( .fetch_one(&data.db) .await; + id.forget(); + match rec { Ok(s) => { if Config::verify(&s.password, &payload.password)? { diff --git a/src/api/v1/mcaptcha.rs b/src/api/v1/mcaptcha.rs index cb6ab26f..282b08e4 100644 --- a/src/api/v1/mcaptcha.rs +++ b/src/api/v1/mcaptcha.rs @@ -38,9 +38,15 @@ pub async fn add_domain( is_authenticated(&id)?; let url = Url::parse(&payload.name)?; if let Some(host) = url.host_str() { - sqlx::query!("INSERT INTO mcaptcha_domains (name) VALUES ($1)", host,) - .execute(&data.db) - .await?; + let user = id.identity().unwrap(); + sqlx::query!( + "insert into mcaptcha_domains (name, ID) values + ($1, (select ID from mcaptcha_users where name = ($2) ));", + host, + user + ) + .execute(&data.db) + .await?; Ok(HttpResponse::Ok()) } else { Err(ServiceError::NotAUrl) @@ -65,6 +71,60 @@ pub async fn delete_domain( } } +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TokenName { + pub name: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TokenKeyPair { + pub name: String, + pub key: String, +} + +//#[post("/api/v1/mcaptcha/domain/token/add")] +//pub async fn add_mcaptcha( +// payload: web::Json, +// data: web::Data, +// id: Identity, +//) -> ServiceResult { +// is_authenticated(&id)?; +// let key = get_random(32); +// let res = sqlx::query!( +// "INSERT INTO mcaptcha_config (name, key) VALUES ($1, $2)", +// &payload.name, +// &key, +// ) +// .execute(&data.db) +// .await; +// +// match res { +// Err(e) => Err(dup_error(e, ServiceError::UsernameTaken)), +// Ok(_) => { +// let resp = TokenKeyPair { +// key, +// name: payload.name, +// }; +// +// Ok(HttpResponse::Ok().json(resp)) +// } +// } +//} + +fn get_random(len: usize) -> String { + use std::iter; + + use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; + + let mut rng: ThreadRng = thread_rng(); + + iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(len) + .collect::() +} + #[cfg(test)] mod tests { use actix_web::http::{header, StatusCode}; @@ -77,7 +137,7 @@ mod tests { #[actix_rt::test] async fn add_domains_work() { - const NAME: &str = "testuserdomain"; + const NAME: &str = "testuserdomainn"; const PASSWORD: &str = "longpassworddomain"; const EMAIL: &str = "testuserdomain@a.com"; const DOMAIN: &str = "http://example.com"; diff --git a/src/errors.rs b/src/errors.rs index eb5b12b9..8bfef08e 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -37,8 +37,12 @@ use std::convert::From; pub enum ServiceError { #[display(fmt = "internal server error")] InternalServerError, + #[display(fmt = "The value you entered for email is not an email")] //405j NotAnEmail, + #[display(fmt = "The value you entered for URL is not a URL")] //405j + NotAUrl, + #[display(fmt = "Wrong password")] WrongPassword, #[display(fmt = "Username not found")] @@ -54,22 +58,24 @@ pub enum ServiceError { /// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist) #[display(fmt = "Username contains blacklisted words")] BlacklistError, - /// when the value passed contains characters not present /// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7) /// profile #[display(fmt = "username_case_mapped violation")] UsernameCaseMappedError, - /// when the value passed contains profainity - #[display(fmt = "Username not available")] - UsernameTaken, #[display(fmt = "Passsword too short")] PasswordTooShort, #[display(fmt = "Username too long")] PasswordTooLong, - #[display(fmt = "The value you entered for URL is not a URL")] //405j - NotAUrl, + + /// when the a username is already taken + #[display(fmt = "Username not available")] + UsernameTaken, + + /// when the a token name is already taken + #[display(fmt = "token name not available")] + TokenNameTaken, } #[derive(Serialize, Deserialize)] @@ -104,6 +110,7 @@ impl ResponseError for ServiceError { ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST, ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, + ServiceError::TokenNameTaken => StatusCode::BAD_REQUEST, } } } @@ -142,7 +149,6 @@ impl From for ServiceError { fn from(e: sqlx::Error) -> Self { use sqlx::error::Error; use std::borrow::Cow; - debug!("{:?}", &e); if let Error::Database(err) = e { if err.code() == Some(Cow::from("23505")) { return ServiceError::UsernameTaken; @@ -153,5 +159,19 @@ impl From for ServiceError { } } +pub fn dup_error(e: sqlx::Error, dup_error: ServiceError) -> ServiceError { + use sqlx::error::Error; + use std::borrow::Cow; + if let Error::Database(err) = e { + if err.code() == Some(Cow::from("23505")) { + dup_error + } else { + ServiceError::InternalServerError + } + } else { + ServiceError::InternalServerError + } +} + #[cfg(not(tarpaulin_include))] pub type ServiceResult = std::result::Result;