diff --git a/docs/account/id.md b/docs/account/id.md index 91d193f..16847de 100644 --- a/docs/account/id.md +++ b/docs/account/id.md @@ -16,4 +16,6 @@ __Content - JSON:__ | username | The projects unique name. | ### 401 - Error: Unauthorized -The provided access token doesn't allow you to perform this operation. \ No newline at end of file +The provided access token doesn't allow you to perform this operation. +### 403 - Error: Forbidden +Blocked for security reasons. \ No newline at end of file diff --git a/src/accounts.rs b/src/accounts.rs index d6c475a..74a2ec2 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -3,7 +3,7 @@ use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; -use base64::{Engine, engine::general_purpose::STANDARD as Base64Engine}; +use base64::{engine::general_purpose::STANDARD as Base64Engine, Engine}; use sha2::{Digest, Sha256}; use sqlx::{postgres::PgPool, types::chrono as sqlx_chrono}; diff --git a/src/api/account/calls.rs b/src/api/account/calls.rs index f636b6c..f3f7626 100644 --- a/src/api/account/calls.rs +++ b/src/api/account/calls.rs @@ -13,7 +13,9 @@ async fn register( Ok(resp) => match resp { data::RegisterResponse::Success => HttpResponse::Ok().finish(), data::RegisterResponse::Conflict(b) => HttpResponse::Conflict().json(web::Json(b)), - data::RegisterResponse::Unprocessable(b) => HttpResponse::UnprocessableEntity().json(web::Json(b)), + data::RegisterResponse::Unprocessable(b) => { + HttpResponse::UnprocessableEntity().json(web::Json(b)) + } data::RegisterResponse::Blocked => HttpResponse::Forbidden().finish(), }, Err(e) => { @@ -84,6 +86,21 @@ async fn delete( } } +#[get("/account/id")] +async fn id(data: web::Data, auth: BearerAuth) -> impl Responder { + match handlers::id(&data.pool, auth.token().to_string()).await { + Ok(resp) => match resp { + data::IdResponse::Success(b) => HttpResponse::Ok().json(web::Json(b)), + data::IdResponse::Unauthorized => HttpResponse::Unauthorized().finish(), + data::IdResponse::Blocked => HttpResponse::Forbidden().finish(), + }, + Err(e) => { + error!("While handling delete request: {e}"); + HttpResponse::InternalServerError().finish() + } + } +} + #[delete("/account/tokens")] async fn tokens_delete( data: web::Data, diff --git a/src/api/account/data.rs b/src/api/account/data.rs index 8cf590d..c4b1723 100644 --- a/src/api/account/data.rs +++ b/src/api/account/data.rs @@ -73,6 +73,19 @@ pub enum DeleteResponse { Unauthorized, } +#[derive(Debug, Serialize)] +pub struct IdSuccess { + pub id: i64, + pub username: String, +} + +#[derive(Debug, Serialize)] +pub enum IdResponse { + Success(IdSuccess), + Unauthorized, + Blocked, +} + #[derive(Debug, Deserialize)] pub struct TokenDeleteRequest { pub token: String, diff --git a/src/api/account/handlers.rs b/src/api/account/handlers.rs index 5022d38..5ee4370 100644 --- a/src/api/account/handlers.rs +++ b/src/api/account/handlers.rs @@ -1,7 +1,7 @@ use crate::{ accounts::Account, api::account::data, - security::{is_sql_injection, AlphaExt, meet_password_criteria}, + security::{is_sql_injection, meet_password_criteria, AlphaExt}, tokens::{AuthToken, VerificationToken}, }; use anyhow::Result; @@ -52,11 +52,15 @@ pub async fn register( )?; if !email_regex.is_match(&request.email) { - return Ok(data::RegisterResponse::Unprocessable(data::RegisterUnprocessable::Email)); + return Ok(data::RegisterResponse::Unprocessable( + data::RegisterUnprocessable::Email, + )); } if !meet_password_criteria(&request.password) { - return Ok(data::RegisterResponse::Unprocessable(data::RegisterUnprocessable::Password)); + return Ok(data::RegisterResponse::Unprocessable( + data::RegisterUnprocessable::Password, + )); } let account = Account::new(pool, &request.username, &request.email, &request.password).await?; @@ -172,6 +176,31 @@ pub async fn delete( Ok(data::DeleteResponse::Success) } +pub async fn id(pool: &PgPool, auth: String) -> Result { + if !auth.is_alpha() { + return Ok(data::IdResponse::Blocked); + } + + let token = match AuthToken::check(pool, &auth).await? { + Some(t) => t, + None => return Ok(data::IdResponse::Unauthorized), + }; + + let account = match Account::from_id(pool, token.account.id()).await? { + Some(a) => a, + None => { + return Err(anyhow::Error::msg( + "Failed to get account data. Account not found in database", + )) + } + }; + + Ok(data::IdResponse::Success(data::IdSuccess { + id: account.id.id(), + username: account.username, + })) +} + pub async fn token_delete( pool: &PgPool, auth: String, diff --git a/src/api/mod.rs b/src/api/mod.rs index a79d19e..8620da7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -91,6 +91,7 @@ pub async fn start(port: u16, pool: PgPool) -> Result<()> { .service(account::calls::verify) .service(account::calls::authenticate) .service(account::calls::delete) + .service(account::calls::id) .service(account::calls::tokens_delete) .service(account::calls::tokens_get) .service(project::calls::create) diff --git a/src/api/project/calls.rs b/src/api/project/calls.rs index 1d9bd4a..155eb6d 100644 --- a/src/api/project/calls.rs +++ b/src/api/project/calls.rs @@ -1,11 +1,15 @@ -use crate::api::ApiState; use crate::api::project::{data, handlers}; -use actix_web::{get, post, web, HttpResponse, Responder, delete}; +use crate::api::ApiState; +use actix_web::{delete, get, post, web, HttpResponse, Responder}; use actix_web_httpauth::extractors::bearer::BearerAuth; use log::error; #[post("/project/create")] -async fn create(data: web::Data, auth: BearerAuth, body: web::Json) -> impl Responder { +async fn create( + data: web::Data, + auth: BearerAuth, + body: web::Json, +) -> impl Responder { match handlers::create(&data.pool, auth.token().to_string(), body.into_inner()).await { Ok(resp) => match resp { data::CreateResponse::Success(b) => HttpResponse::Ok().json(web::Json(b)), @@ -21,8 +25,10 @@ async fn create(data: web::Data, auth: BearerAuth, body: web::Json, query: web::Query) -> impl Responder { - +async fn info( + data: web::Data, + query: web::Query, +) -> impl Responder { let request = if query.id.is_none() || query.name.is_none() { if let Some(id) = query.id { data::InfoRequest::Id(id) @@ -51,8 +57,11 @@ async fn info(data: web::Data, query: web::Query } #[delete("/project/delete")] -async fn delete(data: web::Data, auth: BearerAuth, query: web::Query) -> impl Responder { - +async fn delete( + data: web::Data, + auth: BearerAuth, + query: web::Query, +) -> impl Responder { let request = if query.id.is_none() || query.name.is_none() { if let Some(id) = query.id { data::DeleteRequest::Id(id) diff --git a/src/api/project/data.rs b/src/api/project/data.rs index 5806b5b..1a502a5 100644 --- a/src/api/project/data.rs +++ b/src/api/project/data.rs @@ -1,6 +1,5 @@ use serde::{Deserialize, Serialize}; - #[derive(Debug, Deserialize)] pub struct ProjectIdQuery { pub name: Option, @@ -60,4 +59,4 @@ pub enum DeleteResponse { Unauthorized, Blocked, NotFound, -} \ No newline at end of file +} diff --git a/src/api/project/handlers.rs b/src/api/project/handlers.rs index 0ad5d1b..39ec80c 100644 --- a/src/api/project/handlers.rs +++ b/src/api/project/handlers.rs @@ -1,3 +1,5 @@ +use crate::accounts::Account; +use crate::projects::ProjectMemberFlags; use crate::{ api::project::data, projects::Project, @@ -6,8 +8,6 @@ use crate::{ }; use anyhow::{Error, Result}; use sqlx::PgPool; -use crate::accounts::Account; -use crate::projects::ProjectMemberFlags; pub async fn create( pool: &PgPool, @@ -26,7 +26,7 @@ pub async fn create( if is_sql_injection(&request.name) || is_sql_injection(&request.description) { return Ok(data::CreateResponse::Blocked); } - + if let Some(_) = Project::from_name(pool, &request.name).await? { return Ok(data::CreateResponse::Conflict); } @@ -46,22 +46,28 @@ pub async fn info(pool: &PgPool, request: data::InfoRequest) -> Result Project::from_id(pool, id).await? - } - { + data::InfoRequest::Id(id) => Project::from_id(pool, id).await?, + } { Some(p) => p, - None => return Ok(data::InfoResponse::NotFound) + None => return Ok(data::InfoResponse::NotFound), }; let mut members = Vec::new(); members.reserve(project.members.len()); for &id in project.members.iter() { - members.push((match Account::from_id(pool, id.id()).await? { - Some(m) => m.username, - None => return Err(Error::msg(format!("The account with id {} was not found", id.id()))), - }, - id.id(), - id.flags())); + members.push(( + match Account::from_id(pool, id.id()).await? { + Some(m) => m.username, + None => { + return Err(Error::msg(format!( + "The account with id {} was not found", + id.id() + ))) + } + }, + id.id(), + id.flags(), + )); } Ok(data::InfoResponse::Success(data::InfoSuccess { @@ -94,18 +100,20 @@ pub async fn delete( } Project::from_name(pool, &name).await? } - data::DeleteRequest::Id(id) => Project::from_id(pool, id).await? - } - { + data::DeleteRequest::Id(id) => Project::from_id(pool, id).await?, + } { Some(p) => p, - None => return Ok(data::DeleteResponse::NotFound) + None => return Ok(data::DeleteResponse::NotFound), }; - if !project.member_has_flag(&token.account, ProjectMemberFlags::Owner).await { + if !project + .member_has_flag(&token.account, ProjectMemberFlags::Owner) + .await + { return Ok(data::DeleteResponse::Unauthorized); } project.delete(pool).await?; Ok(data::DeleteResponse::Success) -} \ No newline at end of file +} diff --git a/src/projects.rs b/src/projects.rs index a6e0377..77fe328 100644 --- a/src/projects.rs +++ b/src/projects.rs @@ -53,15 +53,17 @@ impl Project { .fetch_one(pool) .await { - Ok(row) => { - Ok(Some(Self { - id: row.id, - name: row.name, - description: row.description, - created: row.created, - members: row.members.iter().map(|&raw_id| raw_id.into()).collect::>(), - })) - }, + Ok(row) => Ok(Some(Self { + id: row.id, + name: row.name, + description: row.description, + created: row.created, + members: row + .members + .iter() + .map(|&raw_id| raw_id.into()) + .collect::>(), + })), Err(sqlx::Error::RowNotFound) => Ok(None), Err(e) => Err(Error::new(e)), } @@ -72,15 +74,17 @@ impl Project { .fetch_one(pool) .await { - Ok(row) => { - Ok(Some(Self { - id: row.id, - name: row.name, - description: row.description, - created: row.created, - members: row.members.iter().map(|&raw_id| raw_id.into()).collect::>(), - })) - }, + Ok(row) => Ok(Some(Self { + id: row.id, + name: row.name, + description: row.description, + created: row.created, + members: row + .members + .iter() + .map(|&raw_id| raw_id.into()) + .collect::>(), + })), Err(sqlx::Error::RowNotFound) => Ok(None), Err(e) => Err(Error::new(e)), } @@ -99,12 +103,9 @@ impl Project { } pub async fn delete(&self, pool: &PgPool) -> Result<()> { - sqlx::query!( - r#"DELETE FROM Projects WHERE id = $1;"#, - self.id - ) - .execute(pool) - .await?; + sqlx::query!(r#"DELETE FROM Projects WHERE id = $1;"#, self.id) + .execute(pool) + .await?; Ok(()) } diff --git a/src/security.rs b/src/security.rs index 1ba6aae..3733a4f 100644 --- a/src/security.rs +++ b/src/security.rs @@ -6,14 +6,18 @@ pub fn is_sql_injection(string: &String) -> bool { } pub fn meet_password_criteria(string: &String) -> bool { - if string.len() < 12 || - !string.chars().any(|c| c.is_numeric()) || - !string.chars().any(|c| c.is_lowercase()) || - !string.chars().any(|c| c.is_uppercase()) || - !string.chars().any(|c|{ + if string.len() < 12 + || !string.chars().any(|c| c.is_numeric()) + || !string.chars().any(|c| c.is_lowercase()) + || !string.chars().any(|c| c.is_uppercase()) + || !string.chars().any(|c| { let x = c as u8; - (x >= 32 && x <= 47) || (x >= 58 && x <= 64) || (x >= 91 && x <= 96) || (x >= 123 && x <= 126) - }) { + (x >= 32 && x <= 47) + || (x >= 58 && x <= 64) + || (x >= 91 && x <= 96) + || (x >= 123 && x <= 126) + }) + { return false; } true